From 7daf26ce39f51ac2a7f50fa684b37987fd8d8d2e Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Mon, 24 Oct 2022 15:44:13 -0700 Subject: [PATCH 001/343] Update e2e test command for consistency --- plugins/woocommerce/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index bddcf4c5ef0..1942d54bc0f 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -31,7 +31,7 @@ "docker:up": "pnpm exec wc-e2e docker:up", "env:dev": "pnpm wp-env start", "env:test": "pnpm run env:dev && ./tests/e2e-pw/bin/test-env-setup.sh", - "e2e-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/e2e-pw/playwright.config.js", + "test:e2e-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/e2e-pw/playwright.config.js", "env:test:cot": "pnpm run env:dev && ./tests/e2e-pw/bin/test-env-setup.sh --cot", "env:performance-init": "./tests/performance/bin/init-sample-products.sh", "env:down": "pnpm wp-env stop", From e916ac3fcaa2f1ce4b6fdcc6a85aee36648610de Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Mon, 24 Oct 2022 15:51:13 -0700 Subject: [PATCH 002/343] README update --- plugins/woocommerce/tests/e2e-pw/README.md | 113 ++++++++++----------- 1 file changed, 54 insertions(+), 59 deletions(-) diff --git a/plugins/woocommerce/tests/e2e-pw/README.md b/plugins/woocommerce/tests/e2e-pw/README.md index 5016e479715..cc63c6a0d0d 100644 --- a/plugins/woocommerce/tests/e2e-pw/README.md +++ b/plugins/woocommerce/tests/e2e-pw/README.md @@ -4,22 +4,22 @@ This is the documentation for the new e2e testing setup based on Playwright and ## Table of contents -- [Pre-requisites](#pre-requisites) -- [Introduction](#introduction) -- [About the Environment](#about-the-environment) -- [Test Variables](#test-variables) -- [Guide for writing e2e tests](#guide-for-writing-e2e-tests) - - [Tools for writing tests](#tools-for-writing-tests) - - [Creating test structure](#creating-test-structure) - - [Writing the test](#writing-the-test) -- [Debugging tests](#debugging-tests) +- [Pre-requisites](#pre-requisites) +- [Introduction](#introduction) +- [About the Environment](#about-the-environment) +- [Test Variables](#test-variables) +- [Guide for writing e2e tests](#guide-for-writing-e2e-tests) + - [Tools for writing tests](#tools-for-writing-tests) + - [Creating test structure](#creating-test-structure) + - [Writing the test](#writing-the-test) +- [Debugging tests](#debugging-tests) ## Pre-requisites -- Node.js ([Installation instructions](https://nodejs.org/en/download/)) -- NVM ([Installation instructions](https://github.com/nvm-sh/nvm)) -- PNPM ([Installation instructions](https://pnpm.io/installation)) -- Docker and Docker Compose ([Installation instructions](https://docs.docker.com/engine/install/)) +- Node.js ([Installation instructions](https://nodejs.org/en/download/)) +- NVM ([Installation instructions](https://github.com/nvm-sh/nvm)) +- PNPM ([Installation instructions](https://pnpm.io/installation)) +- Docker and Docker Compose ([Installation instructions](https://docs.docker.com/engine/install/)) Note, that if you are on Mac and you install docker through other methods such as homebrew, for example, your steps to set it up might be different. The commands listed in steps below may also vary. @@ -31,22 +31,24 @@ End-to-end tests are powered by Playwright. The test site is spinned up using `w **Running tests for the first time:** -- `nvm use` -- `pnpm install` -- `pnpm run build --filter=woocommerce` -- `pnpm env:test --filter=woocommerce` +- `nvm use` +- `pnpm install` +- `pnpm run build --filter=woocommerce` +- `cd plugins/woocommerce` +- `pnpm env:test` +- `pnpm test:e2e-pw` To run the test again, re-create the environment to start with a fresh state: -- `pnpm env:destroy --filter=woocommerce` -- `pnpm env:test --filter=woocommerce` +- `pnpm env:destroy` +- `pnpm env:test` Other ways of running tests: -- `pnpm env:test --filter=woocommerce` (headless) -- `cd plugin/woocommerce && USE_WP_ENV=1 pnpm playwright test --config=tests/e2e-pw/playwright.config.js --headed` (headed) -- `cd plugins/woocommerce && USE_WP_ENV=1 pnpm playwright test --config=tests/e2e-pw/playwright.config.js --debug` (debug) -- `cd plugins/woocommerce && USE_WP_ENV=1 pnpm playwright test --config=tests/e2e-pw/playwright.config.js ./tests/e2e-pw/tests/activate-and-setup/basic-setup.spec.js` (running a single test) +- `pnpm test:e2e-pw` (usual, headless run) +- `pnpm test:e2e-pw --headed` (headed -- browser window shown) +- `pnpm test:e2e-pw --debug` (runs tests in debug mode) +- `pnpm test:e2e-pw ./tests/e2e-pw/tests/activate-and-setup/basic-setup.spec.js` (runs a single test) To see all options, run `cd plugins/woocommerce && pnpm playwright test --help` @@ -54,15 +56,14 @@ To see all options, run `cd plugins/woocommerce && pnpm playwright test --help` The default values are: -- Latest stable WordPress version -- PHP 7.4 -- MariaDB -- URL: `http://localhost:8086/` -- Admin credentials: `admin/password` +- Latest stable WordPress version +- PHP 7.4 +- MariaDB +- URL: `http://localhost:8086/` +- Admin credentials: `admin/password` If you want to customize these, check the [Test Variables](#test-variables) section. - For more information how to configure the test environment for `wp-env`, please checkout the [documentation](https://github.com/WordPress/gutenberg/tree/trunk/packages/env) documentation. ### Test Variables @@ -70,18 +71,18 @@ For more information how to configure the test environment for `wp-env`, please The test environment uses the following test variables: ```json -{ - "url": "http://localhost:8086/", - "users": { - "admin": { - "username": "admin", - "password": "password" - }, - "customer": { - "username": "customer", - "password": "password" - } - } +{ + "url": "http://localhost:8086/", + "users": { + "admin": { + "username": "admin", + "password": "password" + }, + "customer": { + "username": "customer", + "password": "password" + } + } } ``` @@ -93,14 +94,14 @@ Edit [.wp-env.json](https://github.com/woocommerce/woocommerce/blob/trunk/plugin **Modiify port for e2e-environment** -Edit [tests/e2e/config/default.json](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/e2e/config/default.json).**** +Edit [tests/e2e/config/default.json](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/e2e/config/default.json).\*\*\*\* ### Starting/stopping the environment After you run a test, it's best to restart the environment to start from a fresh state. We are currently working to reset the state more efficiently to avoid the restart being needed, but this is a work-in-progress. -- `pnpm env:down --filter=woocommerce` to stop the environment -- `pnpm env:destroy --filter=woocommerce` when you make changes to `.wp-env.json` +- `pnpm env:down --filter=woocommerce` to stop the environment +- `pnpm env:destroy --filter=woocommerce` when you make changes to `.wp-env.json` ## Guide for writing e2e tests @@ -108,10 +109,10 @@ After you run a test, it's best to restart the environment to start from a fresh It is a good practice to start working on the test by identifying what needs to be tested on the higher and lower levels. For example, if you are writing a test to verify that merchant can create a virtual product, the overview of the test will be as follows: -- Merchant can create virtual product - - Merchant can log in - - Merchant can create virtual product - - Merchant can verify that virtual product was created +- Merchant can create virtual product + - Merchant can log in + - Merchant can create virtual product + - Merchant can verify that virtual product was created Once you identify the structure of the test, you can move on to writing it. @@ -119,24 +120,18 @@ Once you identify the structure of the test, you can move on to writing it. The structure of the test serves as a skeleton for the test itself. You can turn it into a test by using `describe()` and `it()` methods of Playwright: -- [`test.describe()`](https://playwright.dev/docs/api/class-test#test-describe) - creates a block that groups together several related tests; -- [`test()`](https://playwright.dev/docs/api/class-test#test-call) - actual method that runs the test. +- [`test.describe()`](https://playwright.dev/docs/api/class-test#test-describe) - creates a block that groups together several related tests; +- [`test()`](https://playwright.dev/docs/api/class-test#test-call) - actual method that runs the test. Based on our example, the test skeleton would look as follows: ```js test.describe( 'Merchant can create virtual product', () => { - test( 'merchant can log in', async () => { + test( 'merchant can log in', async () => {} ); - } ); + test( 'merchant can create virtual product', async () => {} ); - test( 'merchant can create virtual product', async () => { - - } ); - - test( 'merchant can verify that virtual product was created', async () => { - - } ); + test( 'merchant can verify that virtual product was created', async () => {} ); } ); ``` From 807ed2821fbe4c7a7da2e70a139aa8eead4b71c2 Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Mon, 24 Oct 2022 15:52:51 -0700 Subject: [PATCH 003/343] Add changelog --- plugins/woocommerce/changelog/e2e-update-command-for-e2e | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/e2e-update-command-for-e2e diff --git a/plugins/woocommerce/changelog/e2e-update-command-for-e2e b/plugins/woocommerce/changelog/e2e-update-command-for-e2e new file mode 100644 index 00000000000..d6398300f19 --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-update-command-for-e2e @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update pnpm command to run e2e tests for consistency. Also update docs with new command. 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 004/343] 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 dc7a233cb15a79e883fea6aea634e13765183020 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Thu, 8 Dec 2022 23:53:36 +0800 Subject: [PATCH 005/343] Add Channels card into Marketing page. --- .../Channels/Channels.tsx | 44 +++++++++++++++ .../overview-multichannel/Channels/index.ts | 1 + .../overview-multichannel/Channels/types.ts | 25 +++++++++ .../Channels/useChannels.ts | 20 +++++++ .../Channels/useRecommendedChannels.ts | 54 +++++++++++++++++++ .../Channels/useRegisteredChannels.ts | 18 +++++++ .../MarketingOverviewMultichannel.tsx | 2 + 7 files changed, 164 insertions(+) create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/index.ts create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/types.ts create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useChannels.ts create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx new file mode 100644 index 00000000000..d1bb669719f --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -0,0 +1,44 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { CardBody } from '@wordpress/components'; +import { Spinner } from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import { Card } from '~/marketing/components'; +import { useChannels } from './useChannels'; + +export const Channels = () => { + const { + loading, + data: { registeredChannels, recommendedChannels }, + } = useChannels(); + + if ( loading ) { + return ( + + + + + + ); + } + + const description = + registeredChannels.length === 0 && + recommendedChannels.length > 0 && + __( 'Start by adding a channel to your store', 'woocommerce' ); + + return ( + + { /* TODO: */ } + Body + + ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/index.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/index.ts new file mode 100644 index 00000000000..da0d9c56072 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/index.ts @@ -0,0 +1 @@ +export { Channels } from './Channels'; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/types.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/types.ts new file mode 100644 index 00000000000..b0af93dd395 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/types.ts @@ -0,0 +1,25 @@ +// TODO: The following types are copied from plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/types.ts. +// They are may be changed later, depending on the outcome of API development. + +type Subcategory = { + slug: string; + name: string; +}; + +type Tag = { + slug: string; + name: string; +}; + +export type RecommendedChannel = { + title: string; + description: string; + url: string; + direct_install: boolean; + icon: string; + product: string; + plugin: string; + categories: Array< string >; + subcategories: Array< Subcategory >; + tags: Array< Tag >; +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useChannels.ts new file mode 100644 index 00000000000..122b466ddf7 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useChannels.ts @@ -0,0 +1,20 @@ +/** + * Internal dependencies + */ +import { useRecommendedChannels } from './useRecommendedChannels'; +import { useRegisteredChannels } from './useRegisteredChannels'; + +export const useChannels = () => { + const { loading: loadingRegistered, data: dataRegistered } = + useRegisteredChannels(); + const { loading: loadingRecommended, data: dataRecommended } = + useRecommendedChannels(); + + return { + loading: loadingRegistered || loadingRecommended, + data: { + registeredChannels: dataRegistered, + recommendedChannels: dataRecommended, + }, + }; +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts new file mode 100644 index 00000000000..a7bf23df8d5 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts @@ -0,0 +1,54 @@ +/** + * Internal dependencies + */ +import { RecommendedChannel } from './types'; + +type UseRecommendedChannels = { + loading: boolean; + data: Array< RecommendedChannel >; +}; + +export const useRecommendedChannels = (): UseRecommendedChannels => { + // TODO: call API here to get data. + // The following are just dummy data for testing now. + return { + loading: false, + data: [ + { + title: 'Facebook for WooCommerce', + description: + 'List your products and create ads on Facebook and Instagram.', + url: 'https://woocommerce.com/products/facebook/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', + direct_install: true, + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/facebook.svg', + product: 'facebook-for-woocommerce', + plugin: 'facebook-for-woocommerce/facebook-for-woocommerce.php', + categories: [ 'marketing' ], + subcategories: [ + { slug: 'sales-channels', name: 'Sales channels' }, + ], + tags: [ + { + slug: 'built-by-woocommerce', + name: 'Built by WooCommerce', + }, + ], + }, + { + title: 'Amazon, eBay & Walmart Integration for WooCommerce', + description: + 'Get the official Amazon, eBay and Walmart extension and create, sync and manage multichannel listings directly from WooCommerce.', + url: 'https://woocommerce.com/products/amazon-ebay-integration/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', + direct_install: false, + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/amazon-ebay.svg', + product: 'amazon-ebay-integration', + plugin: 'woocommerce-amazon-ebay-integration/woocommerce-amazon-ebay-integration.php', + categories: [ 'marketing' ], + subcategories: [ + { slug: 'sales-channels', name: 'Sales channels' }, + ], + tags: [], + }, + ], + }; +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts new file mode 100644 index 00000000000..ee7849e8f70 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts @@ -0,0 +1,18 @@ +export const useRegisteredChannels = () => { + // TODO: call API here to get data. + // The following are just dummy data for testing now. + return { + loading: false, + data: [ + { + name: 'Google Listings and Ads', + description: + 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', + isSetupCompleted: true, + setupUrl: 'www.google.com/setup', + manageUrl: 'www.google.com/manage', + }, + ], + }; +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx index 3afe9c3295a..cffa5655873 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx @@ -7,6 +7,7 @@ import { useUser } from '@woocommerce/data'; * Internal dependencies */ import { getAdminSetting } from '~/utils/admin-settings'; +import { Channels } from './Channels'; import { InstalledExtensions } from './InstalledExtensions'; import { DiscoverTools } from './DiscoverTools'; import { LearnMarketing } from './LearnMarketing'; @@ -22,6 +23,7 @@ export const MarketingOverviewMultichannel: React.FC = () => { return (
+ { shouldShowExtensions && } From 52166434658e68e25f2d1536448a56376e67ac1e Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 9 Dec 2022 02:19:36 +0800 Subject: [PATCH 006/343] Code refactor with CenteredSpinner. --- .../CenteredSpinner/CenteredSpinner.scss | 4 ++++ .../CenteredSpinner/CenteredSpinner.tsx | 17 +++++++++++++++++ .../components/CenteredSpinner/index.ts | 1 + .../client/marketing/components/index.js | 1 + .../DiscoverTools/DiscoverTools.scss | 8 -------- .../DiscoverTools/DiscoverTools.tsx | 10 ++++++---- 6 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/CenteredSpinner.scss create mode 100644 plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/CenteredSpinner.tsx create mode 100644 plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/index.ts diff --git a/plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/CenteredSpinner.scss b/plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/CenteredSpinner.scss new file mode 100644 index 00000000000..de66e759c34 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/CenteredSpinner.scss @@ -0,0 +1,4 @@ +.woocommerce-centered-spinner { + display: flex; + justify-content: center; +} diff --git a/plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/CenteredSpinner.tsx b/plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/CenteredSpinner.tsx new file mode 100644 index 00000000000..19ec4959533 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/CenteredSpinner.tsx @@ -0,0 +1,17 @@ +/** + * External dependencies + */ +import { Spinner } from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import './CenteredSpinner.scss'; + +export const CenteredSpinner = () => { + return ( +
+ +
+ ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/index.ts b/plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/index.ts new file mode 100644 index 00000000000..cb45370f30c --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/index.ts @@ -0,0 +1 @@ +export { CenteredSpinner } from './CenteredSpinner'; diff --git a/plugins/woocommerce-admin/client/marketing/components/index.js b/plugins/woocommerce-admin/client/marketing/components/index.js index c514c3ca633..9131b5f57a5 100644 --- a/plugins/woocommerce-admin/client/marketing/components/index.js +++ b/plugins/woocommerce-admin/client/marketing/components/index.js @@ -5,3 +5,4 @@ export { default as Slider } from './slider'; export { default as ReadBlogMessage } from './ReadBlogMessage'; export { CollapsibleCard, CardBody, CardDivider } from './CollapsibleCard'; export { PluginCardBody } from './PluginCardBody'; +export { CenteredSpinner } from './CenteredSpinner'; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.scss index c11516aec58..ab21090dc41 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.scss @@ -1,11 +1,3 @@ -.woocommerce-marketing-discover-tools-card { - // place the spinner in the center of the card. - .woocommerce-spinner { - display: block; - margin: auto; - } -} - .woocommerce-marketing-discover-tools-card-body-empty-content { width: 50%; margin: auto; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.tsx index ca79f53b085..a116640fbe0 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.tsx @@ -5,12 +5,15 @@ import { __ } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; import { Icon, trendingUp } from '@wordpress/icons'; import { recordEvent } from '@woocommerce/tracks'; -import { Spinner } from '@woocommerce/components'; /** * Internal dependencies */ -import { CollapsibleCard, CardBody } from '~/marketing/components'; +import { + CollapsibleCard, + CardBody, + CenteredSpinner, +} from '~/marketing/components'; import { useRecommendedPlugins } from './useRecommendedPlugins'; import { PluginsTabPanel } from './PluginsTabPanel'; import './DiscoverTools.scss'; @@ -30,7 +33,7 @@ export const DiscoverTools = () => { if ( isInitializing ) { return ( - + ); } @@ -72,7 +75,6 @@ export const DiscoverTools = () => { return ( { renderCardContent() } From a8c8be7c266a3e14bcc0174a35a0de8a63228cfa Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 9 Dec 2022 02:20:05 +0800 Subject: [PATCH 007/343] Use CenteredSpinner in Channels. --- .../marketing/overview-multichannel/Channels/Channels.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index d1bb669719f..a13552a11fa 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -3,12 +3,11 @@ */ import { __ } from '@wordpress/i18n'; import { CardBody } from '@wordpress/components'; -import { Spinner } from '@woocommerce/components'; /** * Internal dependencies */ -import { Card } from '~/marketing/components'; +import { Card, CenteredSpinner } from '~/marketing/components'; import { useChannels } from './useChannels'; export const Channels = () => { @@ -21,7 +20,7 @@ export const Channels = () => { return ( - + ); From 995fb7e02d80b721376562727416317534ca177d Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 9 Dec 2022 19:30:24 +0800 Subject: [PATCH 008/343] Code refactor with CardHeaderTitle component. This component will be used in other components later. --- .../components/CardHeaderTitle/CardHeaderTitle.scss | 6 ++++++ .../components/CardHeaderTitle/CardHeaderTitle.tsx | 12 ++++++++++++ .../marketing/components/CardHeaderTitle/index.ts | 1 + .../components/CollapsibleCard/CollapsibleCard.scss | 4 ---- .../components/CollapsibleCard/CollapsibleCard.tsx | 3 ++- .../client/marketing/components/index.js | 1 + 6 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/CardHeaderTitle.scss create mode 100644 plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/CardHeaderTitle.tsx create mode 100644 plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/index.ts diff --git a/plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/CardHeaderTitle.scss b/plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/CardHeaderTitle.scss new file mode 100644 index 00000000000..b5b6e6ced57 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/CardHeaderTitle.scss @@ -0,0 +1,6 @@ +.woocommerce-marketing-card-header-title { + font-size: 20px; + font-weight: 400; + line-height: 28px; + letter-spacing: 0; +} diff --git a/plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/CardHeaderTitle.tsx b/plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/CardHeaderTitle.tsx new file mode 100644 index 00000000000..6861b8fbe0f --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/CardHeaderTitle.tsx @@ -0,0 +1,12 @@ +/** + * Internal dependencies + */ +import './CardHeaderTitle.scss'; + +export const CardHeaderTitle: React.FC = ( { children } ) => { + return ( +
+ { children } +
+ ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/index.ts b/plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/index.ts new file mode 100644 index 00000000000..424e3f75a78 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/index.ts @@ -0,0 +1 @@ +export { CardHeaderTitle } from './CardHeaderTitle'; diff --git a/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.scss b/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.scss index 5c1aef987c1..4303cbd3e65 100644 --- a/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.scss +++ b/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.scss @@ -1,9 +1,5 @@ .woocommerce-collapsible-card { .components-card-header { - font-size: 20px; - font-weight: 400; - line-height: 28px; - letter-spacing: 0; cursor: pointer; } } diff --git a/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.tsx b/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.tsx index c0a767b1738..c7044810222 100644 --- a/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.tsx +++ b/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.tsx @@ -17,6 +17,7 @@ import classnames from 'classnames'; /** * Internal dependencies */ +import { CardHeaderTitle } from '~/marketing/components'; import './CollapsibleCard.scss'; type CollapsibleCardProps = { @@ -48,7 +49,7 @@ const CollapsibleCard: React.FC< CollapsibleCardProps > = ( { ) } > -
{ header }
+ { header } + ); + } + + return ( + + ); + }; + + return ( + } + name={ plugin.title } + pills={ plugin.tags.map( ( tag ) => ( + { tag.name } + ) ) } + description={ plugin.description } + button={ renderButton() } + /> + ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/index.ts b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/index.ts index e3497c710ba..5e2c782db33 100644 --- a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/index.ts +++ b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/index.ts @@ -1 +1,2 @@ export { PluginCardBody } from './PluginCardBody'; +export { SmartPluginCardBody } from './SmartPluginCardBody'; diff --git a/plugins/woocommerce-admin/client/marketing/components/index.js b/plugins/woocommerce-admin/client/marketing/components/index.js index a1679d3b906..dc971c75160 100644 --- a/plugins/woocommerce-admin/client/marketing/components/index.js +++ b/plugins/woocommerce-admin/client/marketing/components/index.js @@ -4,7 +4,7 @@ export { default as ProductIcon } from './product-icon'; export { default as Slider } from './slider'; export { default as ReadBlogMessage } from './ReadBlogMessage'; export { CollapsibleCard, CardBody, CardDivider } from './CollapsibleCard'; -export { PluginCardBody } from './PluginCardBody'; +export { PluginCardBody, SmartPluginCardBody } from './PluginCardBody'; export { CardHeaderTitle } from './CardHeaderTitle'; export { CardHeaderDescription } from './CardHeaderDescription'; export { CenteredSpinner } from './CenteredSpinner'; From 7852448a7a090b11dbac1dbfceac8416940b6956 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 10 Dec 2022 00:51:57 +0800 Subject: [PATCH 014/343] Use SmartPluginCardBody in Channels component. --- .../Channels/Channels.tsx | 60 ++++++++++++++++--- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index 927fd7c5d3d..c9f33c4c3eb 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -1,8 +1,9 @@ /** * External dependencies */ +import { Fragment } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { Card, CardHeader, CardBody } from '@wordpress/components'; +import { Card, CardHeader, CardBody, CardDivider } from '@wordpress/components'; /** * Internal dependencies @@ -11,6 +12,7 @@ import { CardHeaderTitle, CardHeaderDescription, CenteredSpinner, + SmartPluginCardBody, } from '~/marketing/components'; import { useChannels } from './useChannels'; import './Channels.scss'; @@ -21,6 +23,11 @@ export const Channels = () => { data: { registeredChannels, recommendedChannels }, } = useChannels(); + /** + * TODO: we may need to filter the channels against + * `@woocommerce/data` installed plugins. + */ + if ( loading ) { return ( @@ -36,21 +43,58 @@ export const Channels = () => { ); } - const description = - registeredChannels.length === 0 && - recommendedChannels.length > 0 && - __( 'Start by adding a channel to your store', 'woocommerce' ); + /* + * If users have no registered channels, + * we display recommended channels without collapsible list. + */ + if ( registeredChannels.length === 0 && recommendedChannels.length > 0 ) { + return ( + + + + { __( 'Channels', 'woocommerce' ) } + + + { __( + 'Start by adding a channel to your store', + 'woocommerce' + ) } + + + { recommendedChannels.map( ( el, idx ) => { + return ( + + + { idx < recommendedChannels.length - 1 && ( + + ) } + + ); + } ) } + + ); + } + /* + * TODO: Users have registered channels, + * display the registered channels. + * If there are recommended channels, + * display them in a collapsible list. + */ return ( { __( 'Channels', 'woocommerce' ) } - { description } - { /* TODO: */ } - Body + + { /* TODO: registered channels here. */ } + + { /* TODO: recommended channels here. */ } + { recommendedChannels.length > 0 && ( + recommended + ) } ); }; From 92112e27a161dd3bddb2746b40d3dfeef5bf1f10 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 10 Dec 2022 01:36:28 +0800 Subject: [PATCH 015/343] Add InstalledChannel type. --- .../client/marketing/types/InstalledChannel.ts | 13 +++++++++++++ .../client/marketing/types/index.ts | 5 +++++ 2 files changed, 18 insertions(+) create mode 100644 plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts diff --git a/plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts b/plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts new file mode 100644 index 00000000000..1901ed59f37 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts @@ -0,0 +1,13 @@ +export type SyncStatusType = 'synced' | 'syncing' | 'failed'; +export type IssueTypeType = 'error' | 'warning' | 'none'; + +export type InstalledChannel = { + slug: string; + title: string; + description: string; + icon: string; + syncStatus: SyncStatusType; + issueType: IssueTypeType; + issueText: string; + manageUrl: string; +}; diff --git a/plugins/woocommerce-admin/client/marketing/types/index.ts b/plugins/woocommerce-admin/client/marketing/types/index.ts index 146295d936e..24fbe739fd0 100644 --- a/plugins/woocommerce-admin/client/marketing/types/index.ts +++ b/plugins/woocommerce-admin/client/marketing/types/index.ts @@ -1,2 +1,7 @@ export { InstalledPlugin } from './InstalledPlugin'; export { RecommendedPlugin } from './RecommendedPlugin'; +export { + SyncStatusType, + IssueTypeType, + InstalledChannel, +} from './InstalledChannel'; From a9010ffd68b009e9c51de82e845e3af4ff0dc88c Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 10 Dec 2022 01:37:21 +0800 Subject: [PATCH 016/343] Add InstalledChannelCardBody to Channels. --- .../Channels/Channels.tsx | 16 ++++++++++++- .../Channels/InstalledChannelCardBody.scss | 2 ++ .../Channels/InstalledChannelCardBody.tsx | 24 +++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index c9f33c4c3eb..3b7b62b1670 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -16,6 +16,7 @@ import { } from '~/marketing/components'; import { useChannels } from './useChannels'; import './Channels.scss'; +import { InstalledChannelCardBody } from './InstalledChannelCardBody'; export const Channels = () => { const { @@ -90,10 +91,23 @@ export const Channels = () => {
{ /* TODO: registered channels here. */ } + { registeredChannels.map( ( el, idx ) => { + return ( + + + { idx < registeredChannels.length - 1 && ( + + ) } + + ); + } ) } { /* TODO: recommended channels here. */ } { recommendedChannels.length > 0 && ( - recommended + <> + + recommended + ) } ); diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss new file mode 100644 index 00000000000..2f710f93e97 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss @@ -0,0 +1,2 @@ +.woocommerce-marketing-installed-channel-card-body { +} diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx new file mode 100644 index 00000000000..03daeed32ff --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx @@ -0,0 +1,24 @@ +/** + * External dependencies + */ +import { CardBody } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { InstalledChannel } from '~/marketing/types'; +import './InstalledChannelCardBody.scss'; + +type InstalledChannelCardBodyProps = { + installedChannel: InstalledChannel; +}; + +export const InstalledChannelCardBody: React.FC< + InstalledChannelCardBodyProps +> = ( { installedChannel } ) => { + return ( + + InstalledChannelCardBody + + ); +}; From be621df8da1cf327c88c17b80c1065b5abe2f5b8 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 10 Dec 2022 02:28:13 +0800 Subject: [PATCH 017/343] Add more data for testing. --- .../Channels/useRecommendedChannels.ts | 74 ++++++++++++++++++- .../Channels/useRegisteredChannels.ts | 5 ++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts index a7bf23df8d5..40a43823b47 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts @@ -25,7 +25,10 @@ export const useRecommendedChannels = (): UseRecommendedChannels => { plugin: 'facebook-for-woocommerce/facebook-for-woocommerce.php', categories: [ 'marketing' ], subcategories: [ - { slug: 'sales-channels', name: 'Sales channels' }, + { + slug: 'sales-channels', + name: 'Sales channels', + }, ], tags: [ { @@ -34,6 +37,70 @@ export const useRecommendedChannels = (): UseRecommendedChannels => { }, ], }, + { + title: 'Google Listings and Ads', + description: + 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', + url: 'https://woocommerce.com/products/google-listings-and-ads/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', + direct_install: true, + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', + product: 'google-listings-and-ads', + plugin: 'google-listings-and-ads/google-listings-and-ads.php', + categories: [ 'marketing' ], + subcategories: [ + { + slug: 'sales-channels', + name: 'Sales channels', + }, + ], + tags: [ + { + slug: 'built-by-woocommerce', + name: 'Built by WooCommerce', + }, + ], + }, + { + title: 'Pinterest for WooCommerce', + description: + 'Grow your business on Pinterest! Use this official plugin to allow shoppers to Pin products while browsing your store, track conversions, and advertise on Pinterest.', + url: 'https://woocommerce.com/products/pinterest-for-woocommerce/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', + direct_install: true, + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/pinterest.svg', + product: 'pinterest-for-woocommerce', + plugin: 'pinterest-for-woocommerce/pinterest-for-woocommerce.php', + categories: [ 'marketing' ], + subcategories: [ + { + slug: 'sales-channels', + name: 'Sales channels', + }, + ], + tags: [ + { + slug: 'built-by-woocommerce', + name: 'Built by WooCommerce', + }, + ], + }, + { + title: 'TikTok for WooCommerce', + description: + 'Create advertising campaigns and reach one billion global users with TikTok for WooCommerce.', + url: 'https://woocommerce.com/products/tiktok-for-woocommerce/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', + direct_install: true, + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/tiktok.jpg', + product: 'tiktok-for-business', + plugin: 'tiktok-for-business/tiktok-for-woocommerce.php', + categories: [ 'marketing' ], + subcategories: [ + { + slug: 'sales-channels', + name: 'Sales channels', + }, + ], + tags: [], + }, { title: 'Amazon, eBay & Walmart Integration for WooCommerce', description: @@ -45,7 +112,10 @@ export const useRecommendedChannels = (): UseRecommendedChannels => { plugin: 'woocommerce-amazon-ebay-integration/woocommerce-amazon-ebay-integration.php', categories: [ 'marketing' ], subcategories: [ - { slug: 'sales-channels', name: 'Sales channels' }, + { + slug: 'sales-channels', + name: 'Sales channels', + }, ], tags: [], }, diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts index ee7849e8f70..65a8b7289d0 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts @@ -5,13 +5,18 @@ export const useRegisteredChannels = () => { loading: false, data: [ { + slug: 'google-listings-and-ads', name: 'Google Listings and Ads', + title: 'Google Listings and Ads', description: 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', isSetupCompleted: true, setupUrl: 'www.google.com/setup', manageUrl: 'www.google.com/manage', + syncStatus: 'synced' as const, + issueType: 'none' as const, + issueText: '', }, ], }; From 36cdf7fed5fe789d7a4f418acf725ea2bb7023f3 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 10 Dec 2022 02:41:24 +0800 Subject: [PATCH 018/343] Add changelog. --- ...-34903-multichannel-marketing-frontend-34906-channels-card | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/feature-34903-multichannel-marketing-frontend-34906-channels-card diff --git a/plugins/woocommerce/changelog/feature-34903-multichannel-marketing-frontend-34906-channels-card b/plugins/woocommerce/changelog/feature-34903-multichannel-marketing-frontend-34906-channels-card new file mode 100644 index 00000000000..f1602b854c7 --- /dev/null +++ b/plugins/woocommerce/changelog/feature-34903-multichannel-marketing-frontend-34906-channels-card @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add a new Channels card in multichannel marketing page. From 02ce7cccc5ece0ba6e7aead365836ced0bdf8358 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Tue, 13 Dec 2022 21:35:01 +0800 Subject: [PATCH 019/343] Modify PluginCardBody to accept className, and change description type. --- .../components/PluginCardBody/PluginCardBody.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/PluginCardBody.tsx b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/PluginCardBody.tsx index 1e67edeb3ed..6b0b4acd0c8 100644 --- a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/PluginCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/PluginCardBody.tsx @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * Internal dependencies */ @@ -5,6 +10,7 @@ import { CardBody } from '~/marketing/components'; import './PluginCardBody.scss'; type PluginCardBodyProps = { + className?: string; icon: JSX.Element; name: string; @@ -13,7 +19,7 @@ type PluginCardBodyProps = { */ pills?: Array< JSX.Element >; - description: string; + description: React.ReactNode; button?: JSX.Element; }; @@ -21,6 +27,7 @@ type PluginCardBodyProps = { * Renders a CardBody layout component to display plugin info and button. */ export const PluginCardBody: React.FC< PluginCardBodyProps > = ( { + className, icon, name, pills, @@ -28,7 +35,12 @@ export const PluginCardBody: React.FC< PluginCardBodyProps > = ( { button, } ) => { return ( - +
{ icon }
From 437ebb20a8ea566d8b37033a11afb87f3793f2ca Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Tue, 13 Dec 2022 21:36:09 +0800 Subject: [PATCH 020/343] Display sync status in Channels card. --- .../Channels/InstalledChannelCardBody.scss | 20 ++++++ .../Channels/InstalledChannelCardBody.tsx | 71 +++++++++++++++++-- 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss index 2f710f93e97..16eff2f99db 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss @@ -1,2 +1,22 @@ .woocommerce-marketing-installed-channel-card-body { + .woocommerce-marketing-sync-status { + display: flex; + align-items: center; + gap: $gap-smallest; + + &__failed { + color: $alert-red; + fill: $alert-red; + } + + &__syncing { + color: #008a20; + fill: #008a20; + } + + &__synced { + color: #008a20; + fill: #008a20; + } + } } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx index 03daeed32ff..5552e7601a5 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx @@ -1,24 +1,85 @@ /** * External dependencies */ -import { CardBody } from '@wordpress/components'; +import { Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import GridiconCheckmarkCircle from 'gridicons/dist/checkmark-circle'; +import GridiconSync from 'gridicons/dist/sync'; +import GridiconNotice from 'gridicons/dist/notice'; +import classnames from 'classnames'; /** * Internal dependencies */ -import { InstalledChannel } from '~/marketing/types'; +import { PluginCardBody } from '~/marketing/components'; +import { InstalledChannel, SyncStatusType } from '~/marketing/types'; import './InstalledChannelCardBody.scss'; type InstalledChannelCardBodyProps = { installedChannel: InstalledChannel; }; +type SyncStatusPropsType = { + status: SyncStatusType; +}; + +const iconSize = 18; +const className = 'woocommerce-marketing-sync-status'; + +const SyncStatus: React.FC< SyncStatusPropsType > = ( { status } ) => { + if ( status === 'failed' ) { + return ( +
+ + { __( 'Sync failed', 'woocommerce' ) } +
+ ); + } + + if ( status === 'syncing' ) { + return ( +
+ + { __( 'Syncing', 'woocommerce' ) } +
+ ); + } + + return ( +
+ + { __( 'Synced', 'woocommerce' ) } +
+ ); +}; + export const InstalledChannelCardBody: React.FC< InstalledChannelCardBodyProps > = ( { installedChannel } ) => { return ( - - InstalledChannelCardBody - + + } + name={ installedChannel.title } + description={ +
+ +
+ } + button={ + + } + /> ); }; From e6e6dc19efa38444a811bec6b91552ad3e89397f Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Tue, 13 Dec 2022 22:47:16 +0800 Subject: [PATCH 021/343] Display issue status in Channels card. --- .../Channels/InstalledChannelCardBody.scss | 25 ++++++++++ .../Channels/InstalledChannelCardBody.tsx | 48 ++++++++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss index 16eff2f99db..3a80fa7ef64 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss @@ -1,4 +1,13 @@ .woocommerce-marketing-installed-channel-card-body { + .woocommerce-marketing-installed-channel-description { + display: flex; + gap: $gap-smaller; + + &__separator::before { + content: '•'; + } + } + .woocommerce-marketing-sync-status { display: flex; align-items: center; @@ -19,4 +28,20 @@ fill: #008a20; } } + + .woocommerce-marketing-issue-status { + display: flex; + align-items: center; + gap: $gap-smallest; + + &__error { + color: $alert-red; + fill: $alert-red; + } + + &__warning { + color: $alert-yellow; + fill: $alert-yellow; + } + } } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx index 5552e7601a5..7a734c7669b 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx @@ -57,6 +57,50 @@ const SyncStatus: React.FC< SyncStatusPropsType > = ( { status } ) => { ); }; +type IssueStatusPropsType = { + installedChannel: InstalledChannel; +}; + +const issueStatusClassName = 'woocommerce-marketing-issue-status'; + +const IssueStatus: React.FC< IssueStatusPropsType > = ( { + installedChannel, +} ) => { + if ( installedChannel.issueType === 'error' ) { + return ( +
+ + { installedChannel.issueText } +
+ ); + } + + if ( installedChannel.issueType === 'warning' ) { + return ( +
+ + { installedChannel.issueText } +
+ ); + } + + return ( +
+ { installedChannel.issueText } +
+ ); +}; + export const InstalledChannelCardBody: React.FC< InstalledChannelCardBodyProps > = ( { installedChannel } ) => { @@ -71,8 +115,10 @@ export const InstalledChannelCardBody: React.FC< } name={ installedChannel.title } description={ -
+
+
+
} button={ From f713c01f563fc389864f98a997fb2bd5978dbbdb Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Tue, 13 Dec 2022 23:22:27 +0800 Subject: [PATCH 022/343] Add more properties into InstalledChannel type. --- .../client/marketing/types/InstalledChannel.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts b/plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts index 1901ed59f37..c4e735089ac 100644 --- a/plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts +++ b/plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts @@ -6,8 +6,10 @@ export type InstalledChannel = { title: string; description: string; icon: string; + isSetupCompleted: boolean; + setupUrl: string; + manageUrl: string; syncStatus: SyncStatusType; issueType: IssueTypeType; issueText: string; - manageUrl: string; }; From 9963fd07ab29c9197a532d5454b5c2004de7ccef Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Tue, 13 Dec 2022 23:23:13 +0800 Subject: [PATCH 023/343] Display description and button based on channel setup status in Channels card. --- .../Channels/InstalledChannelCardBody.tsx | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx index 7a734c7669b..abd7f85dc5f 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx @@ -104,6 +104,40 @@ const IssueStatus: React.FC< IssueStatusPropsType > = ( { export const InstalledChannelCardBody: React.FC< InstalledChannelCardBodyProps > = ( { installedChannel } ) => { + /** + * The description section in the channel card. + * + * If setup is not completed, this would be the channel description. + * + * If setup is completed, this would be an element with sync status and issue status. + */ + const description = ! installedChannel.isSetupCompleted ? ( + installedChannel.description + ) : ( +
+ +
+ +
+ ); + + /** + * The action button in the channel card. + * + * If setup is not completed, this would be a "Finish setup" primary button. + * + * If setup is completed, this would be a "Manage" secondary button. + */ + const button = ! installedChannel.isSetupCompleted ? ( + + ) : ( + + ); + return ( } name={ installedChannel.title } - description={ -
- -
- -
- } - button={ - - } + description={ description } + button={ button } /> ); }; From 4a2205bcd1707625d9f968a5e572b3fec526775f Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Wed, 14 Dec 2022 01:13:00 +0800 Subject: [PATCH 024/343] Display recommended channels in a collapsible list in Channels card. --- .../Channels/Channels.scss | 4 ++ .../Channels/Channels.tsx | 21 ++++---- .../CollapsibleRecommendedChannels.tsx | 53 +++++++++++++++++++ 3 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss index aff331de94f..a9d5343d54a 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss @@ -4,4 +4,8 @@ align-items: flex-start; gap: $gap-smallest; } + + .components-button.is-link { + text-decoration: none; + } } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index 3b7b62b1670..7013bc34143 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -17,6 +17,7 @@ import { import { useChannels } from './useChannels'; import './Channels.scss'; import { InstalledChannelCardBody } from './InstalledChannelCardBody'; +import { CollapsibleRecommendedChannels } from './CollapsibleRecommendedChannels'; export const Channels = () => { const { @@ -46,7 +47,8 @@ export const Channels = () => { /* * If users have no registered channels, - * we display recommended channels without collapsible list. + * we display recommended channels without collapsible list + * and with a description in the card header. */ if ( registeredChannels.length === 0 && recommendedChannels.length > 0 ) { return ( @@ -77,10 +79,10 @@ export const Channels = () => { } /* - * TODO: Users have registered channels, - * display the registered channels. + * Users have registered channels, + * so here we display the registered channels first. * If there are recommended channels, - * display them in a collapsible list. + * we display them next in a collapsible list. */ return ( @@ -90,7 +92,7 @@ export const Channels = () => { - { /* TODO: registered channels here. */ } + { /* Registered channels section. */ } { registeredChannels.map( ( el, idx ) => { return ( @@ -102,12 +104,11 @@ export const Channels = () => { ); } ) } - { /* TODO: recommended channels here. */ } + { /* Recommended channels section. */ } { recommendedChannels.length > 0 && ( - <> - - recommended - + ) } ); diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx new file mode 100644 index 00000000000..99308d4c4db --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import { Fragment, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { CardBody, CardDivider, Button, Icon } from '@wordpress/components'; +import { chevronUp, chevronDown } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { SmartPluginCardBody } from '~/marketing/components'; +import { RecommendedChannel } from './types'; +import './Channels.scss'; + +type RecommendedChannelsType = { + recommendedChannels: Array< RecommendedChannel >; +}; + +export const CollapsibleRecommendedChannels: React.FC< + RecommendedChannelsType +> = ( { recommendedChannels } ) => { + const [ collapsed, setCollapsed ] = useState( true ); + + return ( + <> + + + + + { ! collapsed && + recommendedChannels.map( ( el, idx ) => { + return ( + + + { idx < recommendedChannels.length - 1 && ( + + ) } + + ); + } ) } + + ); +}; From 6b801097541b0b6ef371c01eb6b79da15534c340 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Wed, 14 Dec 2022 02:07:35 +0800 Subject: [PATCH 025/343] Code refactor with RecommendedChannelsList. --- .../Channels/Channels.tsx | 15 +++----- .../CollapsibleRecommendedChannels.tsx | 18 ++++------ .../Channels/RecommendedChannelsList.tsx | 36 +++++++++++++++++++ 3 files changed, 46 insertions(+), 23 deletions(-) create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index 7013bc34143..5511a5c0957 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -12,12 +12,12 @@ import { CardHeaderTitle, CardHeaderDescription, CenteredSpinner, - SmartPluginCardBody, } from '~/marketing/components'; import { useChannels } from './useChannels'; import './Channels.scss'; import { InstalledChannelCardBody } from './InstalledChannelCardBody'; import { CollapsibleRecommendedChannels } from './CollapsibleRecommendedChannels'; +import { RecommendedChannelsList } from './RecommendedChannelsList'; export const Channels = () => { const { @@ -64,16 +64,9 @@ export const Channels = () => { ) } - { recommendedChannels.map( ( el, idx ) => { - return ( - - - { idx < recommendedChannels.length - 1 && ( - - ) } - - ); - } ) } + ); } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx index 99308d4c4db..57fc3c688b9 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx @@ -9,8 +9,8 @@ import { chevronUp, chevronDown } from '@wordpress/icons'; /** * Internal dependencies */ -import { SmartPluginCardBody } from '~/marketing/components'; import { RecommendedChannel } from './types'; +import { RecommendedChannelsList } from './RecommendedChannelsList'; import './Channels.scss'; type RecommendedChannelsType = { @@ -37,17 +37,11 @@ export const CollapsibleRecommendedChannels: React.FC< /> - { ! collapsed && - recommendedChannels.map( ( el, idx ) => { - return ( - - - { idx < recommendedChannels.length - 1 && ( - - ) } - - ); - } ) } + { ! collapsed && ( + + ) } ); }; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx new file mode 100644 index 00000000000..7b61170f74c --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import { Fragment } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { CardDivider } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { SmartPluginCardBody } from '~/marketing/components'; +import { RecommendedChannel } from './types'; +import './Channels.scss'; + +type RecommendedChannelListPropsType = { + recommendedChannels: Array< RecommendedChannel >; +}; + +export const RecommendedChannelsList: React.FC< + RecommendedChannelListPropsType +> = ( { recommendedChannels } ) => { + return ( + <> + { recommendedChannels.map( ( el, idx ) => { + return ( + + + { idx < recommendedChannels.length - 1 && ( + + ) } + + ); + } ) } + + ); +}; From 6c127d70c830b288e187551fe5b015bf394dfabe Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Wed, 14 Dec 2022 20:11:40 +0800 Subject: [PATCH 026/343] Add dummy data for testing. --- .../Channels/useRegisteredChannels.ts | 67 ++++++++++++++++++- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts index 65a8b7289d0..d02667433e9 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts @@ -1,3 +1,64 @@ +// // TODO: To be removed. This is for testing loading state. +// export const useRegisteredChannels = () => { +// // TODO: call API here to get data. +// // The following are just dummy data for testing now. +// return { +// loading: true, +// data: [], +// }; +// }; + +// // TODO: To be removed. This is for testing isSetupCompleted = false. +// export const useRegisteredChannels = () => { +// // TODO: call API here to get data. +// // The following are just dummy data for testing now. +// return { +// loading: false, +// data: [ +// { +// slug: 'google-listings-and-ads', +// name: 'Google Listings and Ads', +// title: 'Google Listings and Ads', +// description: +// 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', +// icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', +// isSetupCompleted: false, +// setupUrl: 'https://www.example.com/setup', +// manageUrl: 'https://www.example.com/manage', +// syncStatus: 'synced' as const, +// issueType: 'none' as const, +// issueText: 'No issues to resolve', +// }, +// ], +// }; +// }; + +// // TODO: To be removed. This is for testing error state. +// export const useRegisteredChannels = () => { +// // TODO: call API here to get data. +// // The following are just dummy data for testing now. +// return { +// loading: false, +// data: [ +// { +// slug: 'google-listings-and-ads', +// name: 'Google Listings and Ads', +// title: 'Google Listings and Ads', +// description: +// 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', +// icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', +// isSetupCompleted: true, +// setupUrl: 'https://www.example.com/setup', +// manageUrl: 'https://www.example.com/manage', +// syncStatus: 'failed' as const, +// issueType: 'error' as const, +// issueText: '3 issues to resolve', +// }, +// ], +// }; +// }; + +// TODO: To be removed. This is for testing everything works okay. export const useRegisteredChannels = () => { // TODO: call API here to get data. // The following are just dummy data for testing now. @@ -12,11 +73,11 @@ export const useRegisteredChannels = () => { 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', isSetupCompleted: true, - setupUrl: 'www.google.com/setup', - manageUrl: 'www.google.com/manage', + setupUrl: 'https://www.example.com/setup', + manageUrl: 'https://www.example.com/manage', syncStatus: 'synced' as const, issueType: 'none' as const, - issueText: '', + issueText: 'No issues to resolve', }, ], }; From 6f4e05945bab0cf3b27b0d2eeb4f2ba51f6363a7 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Wed, 14 Dec 2022 20:19:20 +0800 Subject: [PATCH 027/343] Add dummy data for testing in useRecommendedChannels. --- .../Channels/useRecommendedChannels.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts index 40a43823b47..79f84e5e25d 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts @@ -8,6 +8,26 @@ type UseRecommendedChannels = { data: Array< RecommendedChannel >; }; +// // TODO: to be removed. This is to test for loading state. +// export const useRecommendedChannels = (): UseRecommendedChannels => { +// // TODO: call API here to get data. +// // The following are just dummy data for testing now. +// return { +// loading: true, +// data: [], +// }; +// }; + +// // TODO: to be removed. This is to test for empty data. +// export const useRecommendedChannels = (): UseRecommendedChannels => { +// TODO: call API here to get data. +// The following are just dummy data for testing now. +// return { +// loading: false, +// data: [], +// }; +// }; + export const useRecommendedChannels = (): UseRecommendedChannels => { // TODO: call API here to get data. // The following are just dummy data for testing now. From 2e8b5227e42c396a8f37df4c4e93babb9862b01b Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Wed, 14 Dec 2022 20:54:29 +0800 Subject: [PATCH 028/343] Move RecommendedChannel type. --- .../Channels/CollapsibleRecommendedChannels.tsx | 2 +- .../overview-multichannel/Channels/RecommendedChannelsList.tsx | 2 +- .../overview-multichannel/Channels/useRecommendedChannels.ts | 2 +- .../Channels/types.ts => types/RecommendedChannel.ts} | 0 plugins/woocommerce-admin/client/marketing/types/index.ts | 1 + 5 files changed, 4 insertions(+), 3 deletions(-) rename plugins/woocommerce-admin/client/marketing/{overview-multichannel/Channels/types.ts => types/RecommendedChannel.ts} (100%) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx index 57fc3c688b9..ff32dedde99 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx @@ -9,7 +9,7 @@ import { chevronUp, chevronDown } from '@wordpress/icons'; /** * Internal dependencies */ -import { RecommendedChannel } from './types'; +import { RecommendedChannel } from '~/marketing/types'; import { RecommendedChannelsList } from './RecommendedChannelsList'; import './Channels.scss'; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx index 7b61170f74c..d88bef12f29 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx @@ -9,7 +9,7 @@ import { CardDivider } from '@wordpress/components'; * Internal dependencies */ import { SmartPluginCardBody } from '~/marketing/components'; -import { RecommendedChannel } from './types'; +import { RecommendedChannel } from '~/marketing/types'; import './Channels.scss'; type RecommendedChannelListPropsType = { diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts index 79f84e5e25d..88982ec5bed 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { RecommendedChannel } from './types'; +import { RecommendedChannel } from '~/marketing/types'; type UseRecommendedChannels = { loading: boolean; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/types.ts b/plugins/woocommerce-admin/client/marketing/types/RecommendedChannel.ts similarity index 100% rename from plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/types.ts rename to plugins/woocommerce-admin/client/marketing/types/RecommendedChannel.ts diff --git a/plugins/woocommerce-admin/client/marketing/types/index.ts b/plugins/woocommerce-admin/client/marketing/types/index.ts index 24fbe739fd0..6a822418804 100644 --- a/plugins/woocommerce-admin/client/marketing/types/index.ts +++ b/plugins/woocommerce-admin/client/marketing/types/index.ts @@ -5,3 +5,4 @@ export { IssueTypeType, InstalledChannel, } from './InstalledChannel'; +export { RecommendedChannel } from './RecommendedChannel'; From 8d8f416fceab7de3f2a15d6dadc5f03e4c26c90d Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 14 Dec 2022 14:33:49 +0000 Subject: [PATCH 029/343] Rename `get_errors_no` to `get_errors_count` --- plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php | 2 +- .../src/Admin/Marketing/MarketingChannelInterface.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php index 9669b9014c6..eecd0debd8d 100644 --- a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php +++ b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php @@ -40,7 +40,7 @@ class InstalledExtensions { 'name' => $channel->get_name(), 'description' => $channel->get_description(), 'product_listings_status' => $channel->get_product_listings_status(), - 'errors_no' => $channel->get_errors_no(), + 'errors_count' => $channel->get_errors_count(), 'icon' => $channel->get_icon_url(), ]; }, diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php index 3a7233f073b..b2dbc3819a2 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php @@ -71,7 +71,7 @@ interface MarketingChannelInterface { * * @return int The number of issues to resolve, or 0 if there are no issues with the channel. */ - public function get_errors_no(): int; + public function get_errors_count(): int; /** * Returns an array of the channel's marketing campaigns. From 7734d41887c27dc37eb4607593d450e554da6048 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 14 Dec 2022 14:38:05 +0000 Subject: [PATCH 030/343] Remove the validation for marketing channel slugs Do not check if the marketing channel's slug exists in the list returned by WooCommerce.com Recommendation API. This essentially allows any third-party extension to register as a marketing channel. --- .../src/Admin/Marketing/MarketingChannels.php | 79 ++----------------- .../MarketingServiceProvider.php | 2 +- .../Admin/Marketing/MarketingChannelsTest.php | 79 ++++++++----------- 3 files changed, 37 insertions(+), 123 deletions(-) diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php index 0fcb6fd1e5e..a5fde99eb32 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php @@ -5,7 +5,7 @@ namespace Automattic\WooCommerce\Admin\Marketing; -use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs; +use Exception; /** * MarketingChannels repository class @@ -20,32 +20,6 @@ class MarketingChannels { */ private $registered_channels = []; - /** - * Array of plugin slugs for allowed marketing channels. - * - * @var string[] - */ - private $allowed_channels; - - /** - * MarketingSpecs repository - * - * @var MarketingSpecs - */ - protected $marketing_specs; - - /** - * Class initialization, invoked by the DI container. - * - * @param MarketingSpecs $marketing_specs The MarketingSpecs class. - * - * @internal - */ - final public function init( MarketingSpecs $marketing_specs ) { - $this->marketing_specs = $marketing_specs; - $this->allowed_channels = $this->get_allowed_channels(); - } - /** * Registers a marketing channel. * @@ -55,14 +29,11 @@ class MarketingChannels { * * @return void * - * @see MarketingChannels::is_channel_allowed() Checks if the marketing channel is allowed to be registered or not. + * @throws Exception If the given marketing channel is already registered. */ 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; + if ( isset( $this->registered_channels[ $channel->get_slug() ] ) ) { + throw new Exception( 'Marketing channel cannot be registered because there is already a channel registered with the same slug!' ); } $this->registered_channels[ $channel->get_slug() ] = $channel; @@ -86,46 +57,6 @@ class MarketingChannels { */ $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() ] ); + return array_values( $channels ); } } diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php index 8e27386ae86..3fd7677c84c 100644 --- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php @@ -38,7 +38,7 @@ class MarketingServiceProvider extends AbstractServiceProvider { */ public function register() { $this->share( MarketingSpecs::class ); - $this->share( MarketingChannels::class )->addArgument( MarketingSpecs::class ); + $this->share( MarketingChannels::class ); $this->share( InstalledExtensions::class )->addArgument( MarketingChannels::class ); } } diff --git a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php index cf5885a0557..fdc112ae370 100644 --- a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php +++ b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php @@ -4,7 +4,6 @@ namespace Automattic\WooCommerce\Tests\Admin\Marketing; use Automattic\WooCommerce\Admin\Marketing\MarketingChannelInterface; use Automattic\WooCommerce\Admin\Marketing\MarketingChannels; -use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs; use WC_Unit_Test_Case; /** @@ -16,29 +15,17 @@ class MarketingChannelsTest extends WC_Unit_Test_Case { * Runs before each test. */ public function setUp(): void { - delete_transient( MarketingSpecs::RECOMMENDED_PLUGINS_TRANSIENT ); + remove_all_filters( 'woocommerce_marketing_channels' ); } /** - * @testdox A marketing channel can be registered using the `register` method if it is in the allowed list. + * @testdox A marketing channel can be registered using the `register` method if the same channel slug is NOT previously registered. */ - public function test_registers_allowed_channels() { + public function test_registers_channel() { $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 ); $marketing_channels->register( $test_channel ); $this->assertNotEmpty( $marketing_channels->get_registered_channels() ); @@ -46,42 +33,33 @@ class MarketingChannelsTest extends WC_Unit_Test_Case { } /** - * @testdox A marketing channel can NOT be registered using the `register` method if it is NOT in the allowed list. + * @testdox A marketing channel can NOT be registered using the `register` method if it is previously registered. */ - 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' ); + public function test_throws_exception_if_registering_existing_channels() { + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->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( [] ); + $test_channel_1_duplicate = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1_duplicate->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); $marketing_channels = new MarketingChannels(); - $marketing_channels->init( $marketing_specs ); - $marketing_channels->register( $test_channel ); + $marketing_channels->register( $test_channel_1 ); - $this->assertEmpty( $marketing_channels->get_registered_channels() ); + $this->expectException( \Exception::class ); + $marketing_channels->register( $test_channel_1_duplicate ); + + $this->assertCount( 1, $marketing_channels->get_registered_channels() ); + $this->assertEquals( $test_channel_1, $marketing_channels->get_registered_channels()[0] ); } /** - * @testdox A marketing channel can be registered using the `woocommerce_marketing_channels` WordPress filter if it is in the allowed list. + * @testdox A marketing channel can be registered using the `woocommerce_marketing_channels` WordPress filter if the same channel slug is NOT previously registered. */ - public function test_registers_allowed_channels_using_wp_filter() { + public function test_registers_channel_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', @@ -97,24 +75,29 @@ class MarketingChannelsTest extends WC_Unit_Test_Case { } /** - * @testdox A marketing channel can NOT be registered using the `woocommerce_marketing_channels` WordPress filter if it NOT is in the allowed list. + * @testdox A marketing channel can NOT be registered using the `woocommerce_marketing_channels` WordPress filter if it is previously registered. */ - 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' ); + public function test_overrides_existing_channel_if_registered_using_wp_filter() { + $marketing_channels = new MarketingChannels(); - set_transient( MarketingSpecs::RECOMMENDED_PLUGINS_TRANSIENT, [] ); + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + + $marketing_channels->register( $test_channel_1 ); + + $test_channel_1_duplicate = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1_duplicate->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); add_filter( 'woocommerce_marketing_channels', - function ( array $channels ) use ( $test_channel ) { - $channels[ $test_channel->get_slug() ] = $test_channel; + function ( array $channels ) use ( $test_channel_1_duplicate ) { + $channels[ $test_channel_1_duplicate->get_slug() ] = $test_channel_1_duplicate; return $channels; } ); - $marketing_channels = new MarketingChannels(); - $this->assertEmpty( $marketing_channels->get_registered_channels() ); + $this->assertCount( 1, $marketing_channels->get_registered_channels() ); + $this->assertEquals( $test_channel_1_duplicate, $marketing_channels->get_registered_channels()[0] ); } } From 91e0a0c065102dfd3a2c89412f798688f52c65e9 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 14 Dec 2022 17:37:34 +0000 Subject: [PATCH 031/343] Revert InstalledExtensions The InstalledExtensions class will be used by the previous generation of Marketing dashboard (if the user has not enabled the new "Marketing" feature); therefore, it's best to restore it to the original code. --- .../src/Admin/API/MarketingOverview.php | 9 +- .../Admin/Marketing/InstalledExtensions.php | 617 +++++++++++++++++- .../src/Internal/Admin/Marketing.php | 9 +- .../MarketingServiceProvider.php | 3 - 4 files changed, 586 insertions(+), 52 deletions(-) diff --git a/plugins/woocommerce/src/Admin/API/MarketingOverview.php b/plugins/woocommerce/src/Admin/API/MarketingOverview.php index 883ce04c1bb..930dcb4c0fc 100644 --- a/plugins/woocommerce/src/Admin/API/MarketingOverview.php +++ b/plugins/woocommerce/src/Admin/API/MarketingOverview.php @@ -125,14 +125,7 @@ class MarketingOverview extends \WC_REST_Data_Controller { * @return \WP_Error|\WP_REST_Response */ public function get_installed_plugins( $request ) { - /** - * InstalledExtensions - * - * @var InstalledExtensions $installed_extensions - */ - $installed_extensions = wc_get_container()->get( InstalledExtensions::class ); - - return rest_ensure_response( $installed_extensions->get_data() ); + return rest_ensure_response( InstalledExtensions::get_data() ); } } diff --git a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php index eecd0debd8d..abf2d6fa26f 100644 --- a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php +++ b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php @@ -5,46 +5,597 @@ 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 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_count' => $channel->get_errors_count(), - 'icon' => $channel->get_icon_url(), - ]; - }, - $this->marketing_channels->get_registered_channels() - ); + 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; } + + /** + * 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'; + + $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/Internal/Admin/Marketing.php b/plugins/woocommerce/src/Internal/Admin/Marketing.php index f11cd2d31d9..47d4ab241bb 100644 --- a/plugins/woocommerce/src/Internal/Admin/Marketing.php +++ b/plugins/woocommerce/src/Internal/Admin/Marketing.php @@ -165,14 +165,7 @@ class Marketing { return $settings; } - /** - * InstalledExtensions helper class. - * - * @var InstalledExtensions $installed_extensions - */ - $installed_extensions = wc_get_container()->get( InstalledExtensions::class ); - - $settings['marketing']['installedExtensions'] = $installed_extensions->get_data(); + $settings['marketing']['installedExtensions'] = InstalledExtensions::get_data(); return $settings; } diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php index 3fd7677c84c..c00e484fe89 100644 --- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php @@ -5,7 +5,6 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders; -use Automattic\WooCommerce\Admin\Marketing\InstalledExtensions; use Automattic\WooCommerce\Admin\Marketing\MarketingChannels; use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs; use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider; @@ -30,7 +29,6 @@ class MarketingServiceProvider extends AbstractServiceProvider { protected $provides = array( MarketingSpecs::class, MarketingChannels::class, - InstalledExtensions::class, ); /** @@ -39,6 +37,5 @@ class MarketingServiceProvider extends AbstractServiceProvider { public function register() { $this->share( MarketingSpecs::class ); $this->share( MarketingChannels::class ); - $this->share( InstalledExtensions::class )->addArgument( MarketingChannels::class ); } } From 53dac1d8e37a1be878a66bdf14f48910c2dd2f0c Mon Sep 17 00:00:00 2001 From: Nima Date: Thu, 15 Dec 2022 15:10:25 +0000 Subject: [PATCH 032/343] Fix code style --- plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php index abf2d6fa26f..b1e9ad82b80 100644 --- a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php +++ b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php @@ -241,7 +241,7 @@ class InstalledExtensions { $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' ) ) { + if ( 'activated' === $data['status'] && function_exists( 'facebook_for_woocommerce' ) ) { $integration = facebook_for_woocommerce()->get_integration(); if ( $integration->is_configured() ) { From 30d316886c568aabcbe5a540b8a510da548b6c72 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 16 Dec 2022 01:13:32 +0800 Subject: [PATCH 033/343] Add empty state Campaigns card. --- .../Campaigns/Campaigns.scss | 27 ++++++++++++ .../Campaigns/Campaigns.tsx | 42 +++++++++++++++++++ .../overview-multichannel/Campaigns/index.ts | 1 + .../MarketingOverviewMultichannel.tsx | 3 ++ 4 files changed, 73 insertions(+) create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/index.ts diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss new file mode 100644 index 00000000000..fe59d585b14 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss @@ -0,0 +1,27 @@ +.woocommerce-marketing-campaigns-card { + .woocommerce-marketing-campaigns-card-body-empty-content { + width: 50%; + margin: auto; + text-align: center; + + svg { + fill: $studio-woocommerce-purple-50; + background-color: #f6f7f7; + border-radius: 28px; + margin-bottom: $gap-smallest; + } + + div.woocommerce-marketing-campaigns-card-body-empty-content-title { + font-size: 13px; + font-weight: 600; + line-height: 16px; + color: $gray-900; + margin-bottom: $gap-smallest; + } + + div { + color: $gray-700; + margin-bottom: $gap-smallest; + } + } +} diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx new file mode 100644 index 00000000000..4e4d2f93ea8 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx @@ -0,0 +1,42 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Button, Card, CardHeader, CardBody } from '@wordpress/components'; +import { Icon, megaphone } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { CardHeaderTitle } from '~/marketing/components'; +import './Campaigns.scss'; + +export const Campaigns = () => { + return ( + + + + { __( 'Campaigns', 'woocommerce' ) } + + + + + +
+ { __( + 'Advertise with marketing campaigns', + 'woocommerce' + ) } +
+
+ { __( + 'Easily create and manage marketing campaigns without leaving WooCommerce.', + 'woocommerce' + ) } +
+
+
+ ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/index.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/index.ts new file mode 100644 index 00000000000..fcceee0efe2 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/index.ts @@ -0,0 +1 @@ +export { Campaigns } from './Campaigns'; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx index cffa5655873..780445ffebd 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx @@ -7,6 +7,7 @@ import { useUser } from '@woocommerce/data'; * Internal dependencies */ import { getAdminSetting } from '~/utils/admin-settings'; +import { Campaigns } from './Campaigns'; import { Channels } from './Channels'; import { InstalledExtensions } from './InstalledExtensions'; import { DiscoverTools } from './DiscoverTools'; @@ -23,6 +24,8 @@ export const MarketingOverviewMultichannel: React.FC = () => { return (
+ { /* TODO: show Campaigns card only when there is at least one registered channel. */ } + { shouldShowExtensions && } From af4f79ab28da6f4cd04191d2a3ca8639ab6b8d5d Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 17 Dec 2022 21:08:14 +0800 Subject: [PATCH 034/343] Add loading state for Campaigns card. --- .../Campaigns/Campaigns.tsx | 81 ++++++++++++------- .../Campaigns/useCampaigns.ts | 15 ++++ 2 files changed, 68 insertions(+), 28 deletions(-) create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/useCampaigns.ts diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx index 4e4d2f93ea8..5ecccc2a6e7 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx @@ -8,35 +8,60 @@ import { Icon, megaphone } from '@wordpress/icons'; /** * Internal dependencies */ -import { CardHeaderTitle } from '~/marketing/components'; +import { CardHeaderTitle, CenteredSpinner } from '~/marketing/components'; +import { useCampaigns } from './useCampaigns'; import './Campaigns.scss'; export const Campaigns = () => { - return ( - - - - { __( 'Campaigns', 'woocommerce' ) } - - - - - -
- { __( - 'Advertise with marketing campaigns', - 'woocommerce' - ) } -
-
- { __( - 'Easily create and manage marketing campaigns without leaving WooCommerce.', - 'woocommerce' - ) } -
-
-
- ); + const { loading, data } = useCampaigns(); + + if ( loading ) { + return ( + + + + { __( 'Campaigns', 'woocommerce' ) } + + + + + + + + ); + } + + if ( data.length === 0 ) { + return ( + + + + { __( 'Campaigns', 'woocommerce' ) } + + + + + +
+ { __( + 'Advertise with marketing campaigns', + 'woocommerce' + ) } +
+
+ { __( + 'Easily create and manage marketing campaigns without leaving WooCommerce.', + 'woocommerce' + ) } +
+
+
+ ); + } + + return
TODO: campaigns here.
; }; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/useCampaigns.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/useCampaigns.ts new file mode 100644 index 00000000000..979e15d5532 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/useCampaigns.ts @@ -0,0 +1,15 @@ +// TODO: testing for loading state. +export const useCampaigns = () => { + return { + loading: true, + data: [], + }; +}; + +// TODO: testing for empty data. +// export const useCampaigns = () => { +// return { +// loading: false, +// data: [], +// }; +// }; From f5799c87217f43910b4917d2a517f40026161beb Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sun, 18 Dec 2022 19:34:16 +0800 Subject: [PATCH 035/343] Display campaign data in table in Campaigns card. --- .../Campaigns/Campaigns.tsx | 37 +++++++- .../Campaigns/useCampaigns.ts | 93 +++++++++++++++++-- 2 files changed, 122 insertions(+), 8 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx index 5ecccc2a6e7..79c2a3c3cad 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx @@ -4,6 +4,7 @@ import { __ } from '@wordpress/i18n'; import { Button, Card, CardHeader, CardBody } from '@wordpress/components'; import { Icon, megaphone } from '@wordpress/icons'; +import { Table } from '@woocommerce/components'; /** * Internal dependencies @@ -63,5 +64,39 @@ export const Campaigns = () => { ); } - return
TODO: campaigns here.
; + return ( + + + + { __( 'Campaigns', 'woocommerce' ) } + + + + el.id ) } + rows={ data.map( ( el ) => { + return [ + { display:
{ el.title }
}, + { display: el.cost }, + ]; + } ) } + rowsPerPage={ 5 } + totalRows={ data.length } + /> + + ); }; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/useCampaigns.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/useCampaigns.ts index 979e15d5532..2c5d66742ed 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/useCampaigns.ts +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/useCampaigns.ts @@ -1,10 +1,10 @@ -// TODO: testing for loading state. -export const useCampaigns = () => { - return { - loading: true, - data: [], - }; -}; +// // TODO: testing for loading state. +// export const useCampaigns = () => { +// return { +// loading: true, +// data: [], +// }; +// }; // TODO: testing for empty data. // export const useCampaigns = () => { @@ -13,3 +13,82 @@ export const useCampaigns = () => { // data: [], // }; // }; + +// TODO: testing with campaigns data. +export const useCampaigns = () => { + return { + loading: false, + data: [ + { + channelSlug: 'google-listings-and-ads', + channelName: 'Google Listings and Ads', + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', + id: 'gla-campaign-01', + title: 'Performance Max 01', + description: 'New Zealand', + cost: '$50', + manageUrl: 'https://www.google.com/manage-campaign', + }, + { + channelSlug: 'google-listings-and-ads', + channelName: 'Google Listings and Ads', + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', + id: 'gla-campaign-02', + title: 'Performance Max 02', + description: 'New Zealand', + cost: '$50', + manageUrl: 'https://www.google.com/manage-campaign', + }, + { + channelSlug: 'google-listings-and-ads', + channelName: 'Google Listings and Ads', + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', + id: 'gla-campaign-03', + title: 'Performance Max 03', + description: 'New Zealand', + cost: '$50', + manageUrl: 'https://www.google.com/manage-campaign', + }, + { + channelSlug: 'google-listings-and-ads', + channelName: 'Google Listings and Ads', + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', + id: 'gla-campaign-04', + title: 'Performance Max 04', + description: 'New Zealand', + cost: '$50', + manageUrl: 'https://www.google.com/manage-campaign', + }, + { + channelSlug: 'google-listings-and-ads', + channelName: 'Google Listings and Ads', + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', + id: 'gla-campaign-05', + title: 'Performance Max 05', + description: 'New Zealand', + cost: '$50', + manageUrl: 'https://www.google.com/manage-campaign', + }, + { + channelSlug: 'google-listings-and-ads', + channelName: 'Google Listings and Ads', + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', + id: 'gla-campaign-06', + title: 'Performance Max 06', + description: 'New Zealand', + cost: '$50', + manageUrl: 'https://www.google.com/manage-campaign', + }, + { + channelSlug: 'google-listings-and-ads', + channelName: 'Google Listings and Ads', + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', + id: 'gla-campaign-07', + title: 'Performance Max 07', + description: 'New Zealand', + cost: '$50', + manageUrl: 'https://www.google.com/manage-campaign', + }, + ], + }; +}; From c444dbd12612332e22a909de4c60ffaeaac7a4e8 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sun, 18 Dec 2022 20:08:21 +0800 Subject: [PATCH 036/343] CSS for campaign rows in Campaigns card. --- .../Campaigns/Campaigns.scss | 18 +++++++++ .../Campaigns/Campaigns.tsx | 37 ++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss index fe59d585b14..450fbe8d8c3 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss @@ -24,4 +24,22 @@ margin-bottom: $gap-smallest; } } + + .woocommerce-marketing-campaign-logo { + img { + display: block; + } + } + + .woocommerce-marketing-campaign-title { + a { + color: #007cba; + font-weight: 600; + text-decoration: none; + } + } + + .woocommerce-marketing-campaign-description { + color: $gray-700; + } } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx index 79c2a3c3cad..febdc4ac894 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx @@ -2,7 +2,15 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { Button, Card, CardHeader, CardBody } from '@wordpress/components'; +import { + Button, + Card, + CardHeader, + CardBody, + Flex, + FlexItem, + FlexBlock, +} from '@wordpress/components'; import { Icon, megaphone } from '@wordpress/icons'; import { Table } from '@woocommerce/components'; @@ -90,7 +98,32 @@ export const Campaigns = () => { ids={ data.map( ( el ) => el.id ) } rows={ data.map( ( el ) => { return [ - { display:
{ el.title }
}, + { + display: ( + + + { + + + + + + { el.title } + + + + { el.description } + + + + + ), + }, { display: el.cost }, ]; } ) } From 32cf8d13419d5d354818aa17e153e73e3463bbda Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sun, 18 Dec 2022 20:37:25 +0800 Subject: [PATCH 037/343] Add pagination to Campaigns card. --- .../Campaigns/Campaigns.scss | 7 +++++ .../Campaigns/Campaigns.tsx | 30 ++++++++++++++----- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss index 450fbe8d8c3..4e664f425b0 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss @@ -42,4 +42,11 @@ .woocommerce-marketing-campaign-description { color: $gray-700; } + + .woocommerce-marketing-campaigns-card-footer { + justify-content: center; + + // Remove the border-top because the table cells already has a border-bottom. + border-top: none; + } } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx index febdc4ac894..c6c0943f395 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx @@ -2,17 +2,19 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; import { Button, Card, CardHeader, CardBody, + CardFooter, Flex, FlexItem, FlexBlock, } from '@wordpress/components'; import { Icon, megaphone } from '@wordpress/icons'; -import { Table } from '@woocommerce/components'; +import { Pagination, Table } from '@woocommerce/components'; /** * Internal dependencies @@ -22,6 +24,7 @@ import { useCampaigns } from './useCampaigns'; import './Campaigns.scss'; export const Campaigns = () => { + const [ page, setPage ] = useState( 1 ); const { loading, data } = useCampaigns(); if ( loading ) { @@ -72,6 +75,11 @@ export const Campaigns = () => { ); } + const perPage = 5; + const total = data.length; + const start = ( page - 1 ) * perPage; + const pagedData = data.slice( start, start + perPage ); + return ( @@ -83,8 +91,7 @@ export const Campaigns = () => {
{ label: __( 'Cost', 'woocommerce' ), }, ] } - ids={ data.map( ( el ) => el.id ) } - rows={ data.map( ( el ) => { + ids={ pagedData.map( ( el ) => el.id ) } + rows={ pagedData.map( ( el ) => { return [ { display: ( @@ -127,9 +134,18 @@ export const Campaigns = () => { { display: el.cost }, ]; } ) } - rowsPerPage={ 5 } - totalRows={ data.length } /> + + { + setPage( newPage ); + } } + /> + ); }; From cbc616a9f32289a4bff551dfd81d92148d1cd60b Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sun, 18 Dec 2022 20:38:07 +0800 Subject: [PATCH 038/343] Set cost column as numeric in Campaigns card. This makes the column right-aligned. --- .../marketing/overview-multichannel/Campaigns/Campaigns.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx index c6c0943f395..e7448cea653 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx @@ -100,6 +100,7 @@ export const Campaigns = () => { { key: 'cost', label: __( 'Cost', 'woocommerce' ), + isNumeric: true, }, ] } ids={ pagedData.map( ( el ) => el.id ) } From c67e4791622eda67e46a0c40faae19908df7f0ce Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Mon, 19 Dec 2022 20:53:31 +0800 Subject: [PATCH 039/343] Add Campaign type. --- .../client/marketing/types/Campaign.ts | 10 ++++++++++ .../woocommerce-admin/client/marketing/types/index.ts | 1 + 2 files changed, 11 insertions(+) create mode 100644 plugins/woocommerce-admin/client/marketing/types/Campaign.ts diff --git a/plugins/woocommerce-admin/client/marketing/types/Campaign.ts b/plugins/woocommerce-admin/client/marketing/types/Campaign.ts new file mode 100644 index 00000000000..a8c6c933c99 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/types/Campaign.ts @@ -0,0 +1,10 @@ +export type Campaign = { + id: string; + title: string; + description: string; + cost: string; + manageUrl: string; + icon: string; + channelName: string; + channelSlug: string; +}; diff --git a/plugins/woocommerce-admin/client/marketing/types/index.ts b/plugins/woocommerce-admin/client/marketing/types/index.ts index 6a822418804..34012e1ce00 100644 --- a/plugins/woocommerce-admin/client/marketing/types/index.ts +++ b/plugins/woocommerce-admin/client/marketing/types/index.ts @@ -1,3 +1,4 @@ +export { Campaign } from './Campaign'; export { InstalledPlugin } from './InstalledPlugin'; export { RecommendedPlugin } from './RecommendedPlugin'; export { From 558ddf26091125ac22c3672f3f03eb6bb021b7ed Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Mon, 19 Dec 2022 20:54:01 +0800 Subject: [PATCH 040/343] Use Campaign type in useCampaigns. --- .../Campaigns/useCampaigns.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/useCampaigns.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/useCampaigns.ts index 2c5d66742ed..dcc70eb089f 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/useCampaigns.ts +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/useCampaigns.ts @@ -1,5 +1,15 @@ +/** + * Internal dependencies + */ +import { Campaign } from '~/marketing/types'; + +type UseCampaignsType = { + loading: boolean; + data: Array< Campaign >; +}; + // // TODO: testing for loading state. -// export const useCampaigns = () => { +// export const useCampaigns = (): UseCampaignsType => { // return { // loading: true, // data: [], @@ -7,7 +17,7 @@ // }; // TODO: testing for empty data. -// export const useCampaigns = () => { +// export const useCampaigns = (): UseCampaignsType => { // return { // loading: false, // data: [], @@ -15,7 +25,7 @@ // }; // TODO: testing with campaigns data. -export const useCampaigns = () => { +export const useCampaigns = (): UseCampaignsType => { return { loading: false, data: [ @@ -35,7 +45,7 @@ export const useCampaigns = () => { icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', id: 'gla-campaign-02', title: 'Performance Max 02', - description: 'New Zealand', + description: '6 countries', cost: '$50', manageUrl: 'https://www.google.com/manage-campaign', }, @@ -45,7 +55,7 @@ export const useCampaigns = () => { icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', id: 'gla-campaign-03', title: 'Performance Max 03', - description: 'New Zealand', + description: '10 countries • 15 Sep - 31 Oct 2022', cost: '$50', manageUrl: 'https://www.google.com/manage-campaign', }, From 4fa4f802e947ba2502a857514ad7eefb82d8adc7 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 23 Dec 2022 21:19:00 +0800 Subject: [PATCH 041/343] Check for empty recommended channels. --- .../overview-multichannel/Channels/Channels.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index 5511a5c0957..38ecb794cd9 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -47,10 +47,18 @@ export const Channels = () => { /* * If users have no registered channels, - * we display recommended channels without collapsible list + * we should display recommended channels without collapsible list * and with a description in the card header. */ - if ( registeredChannels.length === 0 && recommendedChannels.length > 0 ) { + if ( registeredChannels.length === 0 ) { + /** + * If for some reasons we don't have recommended channels, + * then we should not show the Channels card at all. + */ + if ( recommendedChannels.length === 0 ) { + return null; + } + return ( From 06a303fc33da0a9691fa47168faa1418f199f661 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 23 Dec 2022 21:21:52 +0800 Subject: [PATCH 042/343] Types for useRegisteredChannels. --- .../Channels/useRegisteredChannels.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts index d02667433e9..d512f1d4d32 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts @@ -1,3 +1,13 @@ +/** + * Internal dependencies + */ +import { InstalledChannel } from '~/marketing/types'; + +type UseRegisteredChannels = { + loading: boolean; + data: Array< InstalledChannel >; +}; + // // TODO: To be removed. This is for testing loading state. // export const useRegisteredChannels = () => { // // TODO: call API here to get data. @@ -17,7 +27,6 @@ // data: [ // { // slug: 'google-listings-and-ads', -// name: 'Google Listings and Ads', // title: 'Google Listings and Ads', // description: // 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', @@ -42,7 +51,6 @@ // data: [ // { // slug: 'google-listings-and-ads', -// name: 'Google Listings and Ads', // title: 'Google Listings and Ads', // description: // 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', @@ -59,7 +67,7 @@ // }; // TODO: To be removed. This is for testing everything works okay. -export const useRegisteredChannels = () => { +export const useRegisteredChannels = (): UseRegisteredChannels => { // TODO: call API here to get data. // The following are just dummy data for testing now. return { @@ -67,7 +75,6 @@ export const useRegisteredChannels = () => { data: [ { slug: 'google-listings-and-ads', - name: 'Google Listings and Ads', title: 'Google Listings and Ads', description: 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', From 31b635b88865a3f5cb5410ee63591765f86d262b Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 23 Dec 2022 21:53:42 +0800 Subject: [PATCH 043/343] Code refactor: move CSS code to corresponding SCSS file. --- .../overview-multichannel/Channels/Channels.scss | 4 ---- .../Channels/CollapsibleRecommendedChannels.scss | 5 +++++ .../Channels/CollapsibleRecommendedChannels.tsx | 8 ++++---- 3 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss index a9d5343d54a..aff331de94f 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss @@ -4,8 +4,4 @@ align-items: flex-start; gap: $gap-smallest; } - - .components-button.is-link { - text-decoration: none; - } } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss new file mode 100644 index 00000000000..eea3ae685b9 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss @@ -0,0 +1,5 @@ +.woocommerce-marketing-recommended-channels { + .components-button.is-link { + text-decoration: none; + } +} diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx index ff32dedde99..48319ad41ef 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { Fragment, useState } from '@wordpress/element'; +import { useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { CardBody, CardDivider, Button, Icon } from '@wordpress/components'; import { chevronUp, chevronDown } from '@wordpress/icons'; @@ -11,7 +11,7 @@ import { chevronUp, chevronDown } from '@wordpress/icons'; */ import { RecommendedChannel } from '~/marketing/types'; import { RecommendedChannelsList } from './RecommendedChannelsList'; -import './Channels.scss'; +import './CollapsibleRecommendedChannels.scss'; type RecommendedChannelsType = { recommendedChannels: Array< RecommendedChannel >; @@ -23,7 +23,7 @@ export const CollapsibleRecommendedChannels: React.FC< const [ collapsed, setCollapsed ] = useState( true ); return ( - <> +
); }; From af2f4eb904eda7ff31d3c21663b5851cb6a878df Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 23 Dec 2022 22:08:50 +0800 Subject: [PATCH 044/343] CSS for "Add channels" button. --- .../Channels/CollapsibleRecommendedChannels.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss index eea3ae685b9..9a02c3861dc 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss @@ -1,5 +1,8 @@ .woocommerce-marketing-recommended-channels { .components-button.is-link { + font-size: 14px; + font-weight: 600; + line-height: 17px; text-decoration: none; } } From 9e0b71ff1ccfc8a63bdff130e8a3b69aaa98dde1 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 23 Dec 2022 22:12:16 +0800 Subject: [PATCH 045/343] Rename CollapsibleRecommendedChannels to RecommendedChannels. --- .../marketing/overview-multichannel/Channels/Channels.tsx | 4 ++-- ...eRecommendedChannels.scss => RecommendedChannels.scss} | 0 ...bleRecommendedChannels.tsx => RecommendedChannels.tsx} | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) rename plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/{CollapsibleRecommendedChannels.scss => RecommendedChannels.scss} (100%) rename plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/{CollapsibleRecommendedChannels.tsx => RecommendedChannels.tsx} (86%) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index 38ecb794cd9..fcae43a11dd 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -16,7 +16,7 @@ import { import { useChannels } from './useChannels'; import './Channels.scss'; import { InstalledChannelCardBody } from './InstalledChannelCardBody'; -import { CollapsibleRecommendedChannels } from './CollapsibleRecommendedChannels'; +import { RecommendedChannels } from './RecommendedChannels'; import { RecommendedChannelsList } from './RecommendedChannelsList'; export const Channels = () => { @@ -107,7 +107,7 @@ export const Channels = () => { { /* Recommended channels section. */ } { recommendedChannels.length > 0 && ( - ) } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.scss similarity index 100% rename from plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss rename to plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.scss diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.tsx similarity index 86% rename from plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx rename to plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.tsx index 48319ad41ef..94bdd0bf557 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.tsx @@ -11,15 +11,15 @@ import { chevronUp, chevronDown } from '@wordpress/icons'; */ import { RecommendedChannel } from '~/marketing/types'; import { RecommendedChannelsList } from './RecommendedChannelsList'; -import './CollapsibleRecommendedChannels.scss'; +import './RecommendedChannels.scss'; type RecommendedChannelsType = { recommendedChannels: Array< RecommendedChannel >; }; -export const CollapsibleRecommendedChannels: React.FC< - RecommendedChannelsType -> = ( { recommendedChannels } ) => { +export const RecommendedChannels: React.FC< RecommendedChannelsType > = ( { + recommendedChannels, +} ) => { const [ collapsed, setCollapsed ] = useState( true ); return ( From 7bd32ba638b5aeaeb9caa4078350d682af781692 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 24 Dec 2022 00:32:23 +0800 Subject: [PATCH 046/343] Remove useChannels and use useRegisteredChannels and useRecommendedChannels directly. --- .../Channels/Channels.tsx | 33 +++++++++---------- .../Channels/useChannels.ts | 20 ----------- 2 files changed, 15 insertions(+), 38 deletions(-) delete mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useChannels.ts diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index fcae43a11dd..c485bef0bce 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -13,24 +13,25 @@ import { CardHeaderDescription, CenteredSpinner, } from '~/marketing/components'; -import { useChannels } from './useChannels'; -import './Channels.scss'; +import { useRegisteredChannels } from './useRegisteredChannels'; +import { useRecommendedChannels } from './useRecommendedChannels'; import { InstalledChannelCardBody } from './InstalledChannelCardBody'; import { RecommendedChannels } from './RecommendedChannels'; import { RecommendedChannelsList } from './RecommendedChannelsList'; +import './Channels.scss'; export const Channels = () => { - const { - loading, - data: { registeredChannels, recommendedChannels }, - } = useChannels(); + const { loading: loadingRegistered, data: dataRegistered } = + useRegisteredChannels(); + const { loading: loadingRecommended, data: dataRecommended } = + useRecommendedChannels(); /** * TODO: we may need to filter the channels against * `@woocommerce/data` installed plugins. */ - if ( loading ) { + if ( loadingRegistered || loadingRecommended ) { return ( @@ -50,12 +51,12 @@ export const Channels = () => { * we should display recommended channels without collapsible list * and with a description in the card header. */ - if ( registeredChannels.length === 0 ) { + if ( dataRegistered.length === 0 ) { /** * If for some reasons we don't have recommended channels, * then we should not show the Channels card at all. */ - if ( recommendedChannels.length === 0 ) { + if ( dataRecommended.length === 0 ) { return null; } @@ -73,7 +74,7 @@ export const Channels = () => { ); @@ -94,22 +95,18 @@ export const Channels = () => {
{ /* Registered channels section. */ } - { registeredChannels.map( ( el, idx ) => { + { dataRegistered.map( ( el, idx ) => { return ( - { idx < registeredChannels.length - 1 && ( - - ) } + { idx < dataRegistered.length - 1 && } ); } ) } { /* Recommended channels section. */ } - { recommendedChannels.length > 0 && ( - + { dataRecommended.length > 0 && ( + ) }
); diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useChannels.ts deleted file mode 100644 index 122b466ddf7..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useChannels.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Internal dependencies - */ -import { useRecommendedChannels } from './useRecommendedChannels'; -import { useRegisteredChannels } from './useRegisteredChannels'; - -export const useChannels = () => { - const { loading: loadingRegistered, data: dataRegistered } = - useRegisteredChannels(); - const { loading: loadingRecommended, data: dataRecommended } = - useRecommendedChannels(); - - return { - loading: loadingRegistered || loadingRecommended, - data: { - registeredChannels: dataRegistered, - recommendedChannels: dataRecommended, - }, - }; -}; From e950417542514b85325338552d8023c5756862fe Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 24 Dec 2022 00:48:03 +0800 Subject: [PATCH 047/343] Move useRegisteredChannels and useRecommendedChannels into shared hooks directory. --- plugins/woocommerce-admin/client/marketing/hooks/index.ts | 2 ++ .../Channels => hooks}/useRecommendedChannels.ts | 0 .../Channels => hooks}/useRegisteredChannels.ts | 0 .../marketing/overview-multichannel/Channels/Channels.tsx | 6 ++++-- 4 files changed, 6 insertions(+), 2 deletions(-) rename plugins/woocommerce-admin/client/marketing/{overview-multichannel/Channels => hooks}/useRecommendedChannels.ts (100%) rename plugins/woocommerce-admin/client/marketing/{overview-multichannel/Channels => hooks}/useRegisteredChannels.ts (100%) diff --git a/plugins/woocommerce-admin/client/marketing/hooks/index.ts b/plugins/woocommerce-admin/client/marketing/hooks/index.ts index a504ae73f17..0a80d7f7f22 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/index.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/index.ts @@ -1 +1,3 @@ export { useInstalledPlugins } from './useInstalledPlugins'; +export { useRegisteredChannels } from './useRegisteredChannels'; +export { useRecommendedChannels } from './useRecommendedChannels'; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts similarity index 100% rename from plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts rename to plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts similarity index 100% rename from plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts rename to plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index c485bef0bce..32b5d3a2879 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -13,8 +13,10 @@ import { CardHeaderDescription, CenteredSpinner, } from '~/marketing/components'; -import { useRegisteredChannels } from './useRegisteredChannels'; -import { useRecommendedChannels } from './useRecommendedChannels'; +import { + useRegisteredChannels, + useRecommendedChannels, +} from '~/marketing/hooks'; import { InstalledChannelCardBody } from './InstalledChannelCardBody'; import { RecommendedChannels } from './RecommendedChannels'; import { RecommendedChannelsList } from './RecommendedChannelsList'; From 52dd8845cc6e4ce66d2e408e290085173b20f34a Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 24 Dec 2022 01:14:02 +0800 Subject: [PATCH 048/343] Load registered and recommended channels in MarketingOverviewMultichannel. The data will be used to conditionally display Campaigns card later. --- .../Channels/Channels.tsx | 70 ++++++------------- .../MarketingOverviewMultichannel.tsx | 20 +++++- 2 files changed, 40 insertions(+), 50 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index 32b5d3a2879..0f3a07e140a 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -3,65 +3,33 @@ */ import { Fragment } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { Card, CardHeader, CardBody, CardDivider } from '@wordpress/components'; +import { Card, CardHeader, CardDivider } from '@wordpress/components'; /** * Internal dependencies */ -import { - CardHeaderTitle, - CardHeaderDescription, - CenteredSpinner, -} from '~/marketing/components'; -import { - useRegisteredChannels, - useRecommendedChannels, -} from '~/marketing/hooks'; +import { CardHeaderTitle, CardHeaderDescription } from '~/marketing/components'; +import { InstalledChannel, RecommendedChannel } from '~/marketing/types'; import { InstalledChannelCardBody } from './InstalledChannelCardBody'; import { RecommendedChannels } from './RecommendedChannels'; import { RecommendedChannelsList } from './RecommendedChannelsList'; import './Channels.scss'; -export const Channels = () => { - const { loading: loadingRegistered, data: dataRegistered } = - useRegisteredChannels(); - const { loading: loadingRecommended, data: dataRecommended } = - useRecommendedChannels(); - - /** - * TODO: we may need to filter the channels against - * `@woocommerce/data` installed plugins. - */ - - if ( loadingRegistered || loadingRecommended ) { - return ( - - - - { __( 'Channels', 'woocommerce' ) } - - - - - - - ); - } +type ChannelsProps = { + registeredChannels: Array< InstalledChannel >; + recommendedChannels: Array< RecommendedChannel >; +}; +export const Channels: React.FC< ChannelsProps > = ( { + registeredChannels, + recommendedChannels, +} ) => { /* * If users have no registered channels, * we should display recommended channels without collapsible list * and with a description in the card header. */ - if ( dataRegistered.length === 0 ) { - /** - * If for some reasons we don't have recommended channels, - * then we should not show the Channels card at all. - */ - if ( dataRecommended.length === 0 ) { - return null; - } - + if ( registeredChannels.length === 0 ) { return ( @@ -76,7 +44,7 @@ export const Channels = () => { ); @@ -97,18 +65,22 @@ export const Channels = () => { { /* Registered channels section. */ } - { dataRegistered.map( ( el, idx ) => { + { registeredChannels.map( ( el, idx ) => { return ( - { idx < dataRegistered.length - 1 && } + { idx < registeredChannels.length - 1 && ( + + ) } ); } ) } { /* Recommended channels section. */ } - { dataRecommended.length > 0 && ( - + { recommendedChannels.length >= 1 && ( + ) } ); diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx index cffa5655873..7cbad05d6a2 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx @@ -12,18 +12,36 @@ import { InstalledExtensions } from './InstalledExtensions'; import { DiscoverTools } from './DiscoverTools'; import { LearnMarketing } from './LearnMarketing'; import '~/marketing/data'; +import { + useRegisteredChannels, + useRecommendedChannels, +} from '~/marketing/hooks'; import './MarketingOverviewMultichannel.scss'; +import { CenteredSpinner } from '../components'; export const MarketingOverviewMultichannel: React.FC = () => { + const { loading: loadingRegistered, data: dataRegistered } = + useRegisteredChannels(); + const { loading: loadingRecommended, data: dataRecommended } = + useRecommendedChannels(); const { currentUserCan } = useUser(); const shouldShowExtensions = getAdminSetting( 'allowMarketplaceSuggestions', false ) && currentUserCan( 'install_plugins' ); + if ( loadingRegistered || loadingRecommended ) { + return ; + } + return (
- + { ( dataRegistered.length >= 1 || dataRecommended.length >= 1 ) && ( + + ) } { shouldShowExtensions && } From 8e39098256c8fe016a42da7760a48efd37d2eb2c Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 24 Dec 2022 01:43:09 +0800 Subject: [PATCH 049/343] Show Campaigns card only when there is at least one registered channel. --- .../overview-multichannel/MarketingOverviewMultichannel.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx index 3a650b32588..e7858c2c01a 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx @@ -37,8 +37,7 @@ export const MarketingOverviewMultichannel: React.FC = () => { return (
- { /* TODO: show Campaigns card only when there is at least one registered channel. */ } - + { dataRegistered.length >= 1 && } { ( dataRegistered.length >= 1 || dataRecommended.length >= 1 ) && ( Date: Sat, 24 Dec 2022 01:45:59 +0800 Subject: [PATCH 050/343] Remove the unneeded "Create new campaign" button for now. --- .../overview-multichannel/Campaigns/Campaigns.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx index e7448cea653..c422bd7b089 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx @@ -34,9 +34,6 @@ export const Campaigns = () => { { __( 'Campaigns', 'woocommerce' ) } - @@ -52,9 +49,6 @@ export const Campaigns = () => { { __( 'Campaigns', 'woocommerce' ) } - @@ -86,9 +80,6 @@ export const Campaigns = () => { { __( 'Campaigns', 'woocommerce' ) } -
Date: Wed, 28 Dec 2022 13:23:26 +0000 Subject: [PATCH 051/343] Add channel property to MarketingCampaign --- .../src/Admin/Marketing/MarketingCampaign.php | 29 +++++++++++++++---- .../Admin/Marketing/MarketingCampaignTest.php | 16 ++++++++-- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php index 7b3f99a4b3a..45037fea367 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php @@ -22,6 +22,13 @@ class MarketingCampaign implements JsonSerializable { */ protected $id; + /** + * The marketing channel that this campaign belongs to. + * + * @var MarketingChannelInterface + */ + protected $channel; + /** * Title of the marketing campaign. * @@ -46,13 +53,15 @@ class MarketingCampaign implements JsonSerializable { /** * MarketingCampaign constructor. * - * @param string $id The marketing campaign's unique identifier. - * @param string $title The title of the marketing campaign. - * @param string $manage_url The URL to the channel's campaign management page. - * @param Price|null $cost The cost of the marketing campaign with the currency. + * @param string $id The marketing campaign's unique identifier. + * @param MarketingChannelInterface $channel The marketing channel that this campaign belongs to. + * @param string $title The title of the marketing campaign. + * @param string $manage_url The URL to the channel's campaign management page. + * @param Price|null $cost The cost of the marketing campaign with the currency. */ - public function __construct( string $id, string $title, string $manage_url, Price $cost = null ) { + public function __construct( string $id, MarketingChannelInterface $channel, string $title, string $manage_url, Price $cost = null ) { $this->id = $id; + $this->channel = $channel; $this->title = $title; $this->manage_url = $manage_url; $this->cost = $cost; @@ -67,6 +76,15 @@ class MarketingCampaign implements JsonSerializable { return $this->id; } + /** + * Returns the marketing channel that this campaign belongs to. + * + * @return MarketingChannelInterface + */ + public function get_channel(): MarketingChannelInterface { + return $this->channel; + } + /** * Returns the title of the marketing campaign. * @@ -102,6 +120,7 @@ class MarketingCampaign implements JsonSerializable { public function jsonSerialize() { return [ 'id' => $this->get_id(), + 'channel' => $this->get_channel()->get_slug(), 'title' => $this->get_title(), 'manage_url' => $this->get_manage_url(), 'cost' => $this->get_cost(), diff --git a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php index 401e1630294..30d8c243ba5 100644 --- a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php +++ b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php @@ -3,6 +3,7 @@ namespace Automattic\WooCommerce\Tests\Admin\Marketing; use Automattic\WooCommerce\Admin\Marketing\MarketingCampaign; +use Automattic\WooCommerce\Admin\Marketing\MarketingChannelInterface; use Automattic\WooCommerce\Admin\Marketing\Price; use WC_Unit_Test_Case; @@ -15,7 +16,10 @@ class MarketingCampaignTest extends WC_Unit_Test_Case { * @testdox `get_id`, `get_title`, `get_manage_url`, and `get_cost` return the class properties set by the constructor. */ public function test_get_methods_return_properties() { - $marketing_campaign = new MarketingCampaign( '1234', 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + + $marketing_campaign = new MarketingCampaign( '1234', $test_channel_1, 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); $this->assertEquals( '1234', $marketing_campaign->get_id() ); $this->assertEquals( 'Ad #1234', $marketing_campaign->get_title() ); @@ -29,7 +33,9 @@ class MarketingCampaignTest extends WC_Unit_Test_Case { * @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' ); + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + + $marketing_campaign = new MarketingCampaign( '1234', $test_channel_1, 'Ad #1234', 'https://example.com/manage-campaigns' ); $this->assertNull( $marketing_campaign->get_cost() ); } @@ -38,13 +44,17 @@ class MarketingCampaignTest extends WC_Unit_Test_Case { * @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' ) ); + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + + $marketing_campaign = new MarketingCampaign( '1234', $test_channel_1, '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(), + 'channel' => 'test-channel-1', 'title' => $marketing_campaign->get_title(), 'manage_url' => $marketing_campaign->get_manage_url(), 'cost' => [ From 7dcdbd871e68cedd24b2c82b50199e64ee0d454d Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:23:42 +0000 Subject: [PATCH 052/343] Add methods to filter the recommended marketing channels and extensions --- .../Admin/Marketing/MarketingSpecs.php | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php b/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php index ee36ba1375c..e6ed4e875d9 100644 --- a/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php +++ b/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php @@ -28,6 +28,20 @@ class MarketingSpecs { */ const KNOWLEDGE_BASE_TRANSIENT = 'wc_marketing_knowledge_base'; + /** + * Slug of the category specifying marketing extensions on the WooCommerce.com store. + * + * @var string + */ + const MARKETING_EXTENSION_CATEGORY_SLUG = 'marketing'; + + /** + * Slug of the subcategory specifying marketing channels on the WooCommerce.com store. + * + * @var string + */ + const MARKETING_CHANNEL_SUBCATEGORY_SLUG = 'sales-channels'; + /** * Load recommended plugins from WooCommerce.com * @@ -61,6 +75,64 @@ class MarketingSpecs { return array_values( $plugins ); } + /** + * Return only the recommended marketing channels from WooCommerce.com. + * + * @return array + */ + public function get_recommended_marketing_channels(): array { + return array_filter( $this->get_recommended_plugins(), [ $this, 'is_marketing_channel_plugin' ] ); + } + + /** + * Return all recommended marketing extensions EXCEPT the marketing channels from WooCommerce.com. + * + * @return array + */ + public function get_recommended_marketing_extensions_excluding_channels(): array { + return array_filter( + $this->get_recommended_plugins(), + function ( array $plugin_data ) { + return $this->is_marketing_plugin( $plugin_data ) && ! $this->is_marketing_channel_plugin( $plugin_data ); + } + ); + } + + /** + * Returns whether a plugin is a marketing extension. + * + * @param array $plugin_data The plugin properties returned by the API. + * + * @return bool + */ + protected function is_marketing_plugin( array $plugin_data ): bool { + $categories = $plugin_data['categories'] ?? []; + + return in_array( self::MARKETING_EXTENSION_CATEGORY_SLUG, $categories, true ); + } + + /** + * Returns whether a plugin is a marketing channel. + * + * @param array $plugin_data The plugin properties returned by the API. + * + * @return bool + */ + protected function is_marketing_channel_plugin( array $plugin_data ): bool { + if ( ! $this->is_marketing_plugin( $plugin_data ) ) { + return false; + } + + $subcategories = $plugin_data['subcategories'] ?? []; + foreach ( $subcategories as $subcategory ) { + if ( isset( $subcategory['slug'] ) && self::MARKETING_CHANNEL_SUBCATEGORY_SLUG === $subcategory['slug'] ) { + return true; + } + } + + return false; + } + /** * Load knowledge base posts from WooCommerce.com * From 677416fcf2066b7913ce58521a89ca6eceadca58 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:25:51 +0000 Subject: [PATCH 053/343] Add `marketing/recommendations` API --- plugins/woocommerce/src/Admin/API/Init.php | 1 + .../Admin/API/MarketingRecommendations.php | 235 ++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 plugins/woocommerce/src/Admin/API/MarketingRecommendations.php diff --git a/plugins/woocommerce/src/Admin/API/Init.php b/plugins/woocommerce/src/Admin/API/Init.php index 42b2bac82e7..45ef28bd4b9 100644 --- a/plugins/woocommerce/src/Admin/API/Init.php +++ b/plugins/woocommerce/src/Admin/API/Init.php @@ -64,6 +64,7 @@ class Init { 'Automattic\WooCommerce\Admin\API\Experiments', 'Automattic\WooCommerce\Admin\API\Marketing', 'Automattic\WooCommerce\Admin\API\MarketingOverview', + 'Automattic\WooCommerce\Admin\API\MarketingRecommendations', 'Automattic\WooCommerce\Admin\API\Options', 'Automattic\WooCommerce\Admin\API\Orders', 'Automattic\WooCommerce\Admin\API\PaymentGatewaySuggestions', diff --git a/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php b/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php new file mode 100644 index 00000000000..a64fcce02d7 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php @@ -0,0 +1,235 @@ +namespace, + '/' . $this->rest_base, + [ + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_items' ], + 'permission_callback' => [ $this, 'get_items_permissions_check' ], + 'args' => [ + 'category' => [ + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + 'sanitize_callback' => 'sanitize_title_with_dashes', + 'enum' => [ 'channels', 'extensions' ], + 'required' => true, + ], + ], + ], + 'schema' => [ $this, 'get_public_item_schema' ], + ] + ); + } + + /** + * Check whether a given request has permission to view marketing recommendations. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'install_plugins' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing channels.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + return true; + } + + /** + * Retrieves a collection of recommendations. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + /** + * MarketingSpecs class. + * + * @var MarketingSpecs $marketing_specs + */ + $marketing_specs = wc_get_container()->get( MarketingSpecs::class ); + + $category = $request->get_param( 'category' ); + if ( 'channels' === $category ) { + $items = $marketing_specs->get_recommended_marketing_channels(); + } elseif ( 'extensions' === $category ) { + $items = $marketing_specs->get_recommended_marketing_extensions_excluding_channels(); + } else { + return new WP_Error( 'woocommerce_rest_invalid_category', __( 'The specified category for recommendations is invalid. Allowed values: "channels", "extensions".', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $responses = []; + foreach ( $items as $item ) { + $response = $this->prepare_item_for_response( $item, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $responses ); + } + + /** + * Prepares the item for the REST response. + * + * @param array $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_recommendation', + 'type' => 'object', + 'properties' => [ + 'title' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'description' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'url' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'direct_install' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'icon' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'product' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'plugin' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'categories' => [ + 'type' => 'array', + 'context' => [ 'view' ], + 'readonly' => true, + 'items' => [ + 'type' => 'string', + ], + ], + 'subcategories' => [ + 'type' => 'array', + 'context' => [ 'view' ], + 'readonly' => true, + 'items' => [ + 'type' => 'object', + 'context' => [ 'view' ], + 'readonly' => true, + 'properties' => [ + 'slug' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + ], + 'tags' => [ + 'type' => 'array', + 'context' => [ 'view' ], + 'readonly' => true, + 'items' => [ + 'type' => 'object', + 'context' => [ 'view' ], + 'readonly' => true, + 'properties' => [ + 'slug' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } +} From 086ce8f48a704740a71bea3ef3dc6fbf976f56c1 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:26:04 +0000 Subject: [PATCH 054/343] Add unit tests for `marketing/recommendations` API --- .../API/MarketingRecommendationsTest.php | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php new file mode 100644 index 00000000000..46335da52e3 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php @@ -0,0 +1,138 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + set_transient( + MarketingSpecs::RECOMMENDED_PLUGINS_TRANSIENT, + [ + [ + 'title' => 'Example Marketing Channel', + 'description' => 'List your products and create ads, etc.', + 'url' => 'https://woocommerce.com/products/example-channel', + 'direct_install' => true, + 'icon' => 'https://woocommerce.com/example.svg', + 'product' => 'example-channel', + 'plugin' => 'example-channel/example-channel.php', + 'categories' => [ MarketingSpecs::MARKETING_EXTENSION_CATEGORY_SLUG ], + 'subcategories' => [ + [ + 'slug' => MarketingSpecs::MARKETING_CHANNEL_SUBCATEGORY_SLUG, + 'name' => 'Sales channels', + ], + ], + 'tags' => [], + ], + [ + 'title' => 'Example Marketing Extension', + 'description' => 'Automate your customer communications, etc.', + 'url' => 'https://woocommerce.com/products/example-marketing-extension', + 'direct_install' => true, + 'icon' => 'https://woocommerce.com/example-marketing-extension.svg', + 'product' => 'example-marketing-extension', + 'plugin' => 'example-marketing-extension/example-marketing-extension.php', + 'categories' => [ MarketingSpecs::MARKETING_EXTENSION_CATEGORY_SLUG ], + 'subcategories' => [ + [ + 'slug' => 'email', + 'name' => 'Email', + ], + ], + 'tags' => [], + ], + [ + 'title' => 'Example NON Marketing Extension', + 'description' => 'Handle coupons, etc.', + 'url' => 'https://woocommerce.com/products/example-random-extension', + 'direct_install' => true, + 'icon' => 'https://woocommerce.com/example-random-extension.svg', + 'product' => 'example-random-extension', + 'plugin' => 'example-random-extension/example-random-extension.php', + 'categories' => [ 'coupons' ], + 'subcategories' => [], + 'tags' => [], + ], + ] + ); + } + + /** + * Tests that the marketing channel recommendations are returned by the endpoint. + */ + public function test_returns_recommended_marketing_channels() { + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( [ 'category' => 'channels' ] ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 1, $data ); + $this->assertEquals( 'Example Marketing Channel', $data[0]['title'] ); + $this->assertEquals( 'example-channel', $data[0]['product'] ); + } + + /** + * Tests that the marketing extension recommendations are returned by the endpoint. + */ + public function test_returns_recommended_marketing_extensions() { + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( [ 'category' => 'extensions' ] ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 1, $data ); + $this->assertEquals( 'Example Marketing Extension', $data[0]['title'] ); + $this->assertEquals( 'example-marketing-extension', $data[0]['product'] ); + } + + /** + * Tests that the endpoint returns an error if the provided category is invalid. + */ + public function test_returns_error_if_invalid_category_provided() { + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( [ 'category' => 'test-non-existing-invalid-category' ] ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + $this->assertEquals( 'rest_invalid_param', $data['code'] ); + } + +} From a1468ec73927b16cc54c567376dcf56fe9377ca2 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:26:40 +0000 Subject: [PATCH 055/343] Add `marketing/channels` API --- plugins/woocommerce/src/Admin/API/Init.php | 1 + .../src/Admin/API/MarketingChannels.php | 192 ++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 plugins/woocommerce/src/Admin/API/MarketingChannels.php diff --git a/plugins/woocommerce/src/Admin/API/Init.php b/plugins/woocommerce/src/Admin/API/Init.php index 45ef28bd4b9..bf2232185ca 100644 --- a/plugins/woocommerce/src/Admin/API/Init.php +++ b/plugins/woocommerce/src/Admin/API/Init.php @@ -65,6 +65,7 @@ class Init { 'Automattic\WooCommerce\Admin\API\Marketing', 'Automattic\WooCommerce\Admin\API\MarketingOverview', 'Automattic\WooCommerce\Admin\API\MarketingRecommendations', + 'Automattic\WooCommerce\Admin\API\MarketingChannels', 'Automattic\WooCommerce\Admin\API\Options', 'Automattic\WooCommerce\Admin\API\Orders', 'Automattic\WooCommerce\Admin\API\PaymentGatewaySuggestions', diff --git a/plugins/woocommerce/src/Admin/API/MarketingChannels.php b/plugins/woocommerce/src/Admin/API/MarketingChannels.php new file mode 100644 index 00000000000..a02039339f1 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingChannels.php @@ -0,0 +1,192 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to view marketing channels. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'install_plugins' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing channels.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + return true; + } + + /** + * Return installed marketing channels. + * + * @param WP_REST_Request $request Request data. + * + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + /** + * MarketingChannels class. + * + * @var MarketingChannelsService $marketing_channels_service + */ + $marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + + $channels = $marketing_channels_service->get_registered_channels(); + + $responses = []; + foreach ( $channels as $item ) { + $response = $this->prepare_item_for_response( $item, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $responses ); + } + + /** + * Prepares the item for the REST response. + * + * @param MarketingChannelInterface $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $data = [ + 'slug' => $item->get_slug(), + 'is_setup_completed' => $item->is_setup_completed(), + 'settings_url' => $item->get_setup_url(), + 'name' => $item->get_name(), + 'description' => $item->get_description(), + 'product_listings_status' => $item->get_product_listings_status(), + 'errors_count' => $item->get_errors_count(), + 'icon' => $item->get_icon_url(), + ]; + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_channel', + 'type' => 'object', + 'properties' => [ + 'slug' => [ + 'description' => __( 'Unique identifier string for the marketing channel extension, also known as the plugin slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'description' => __( 'Name of the marketing channel.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'description' => [ + 'description' => __( 'Description of the marketing channel.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'icon' => [ + 'description' => __( 'Path to the channel icon.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'is_setup_completed' => [ + 'type' => 'boolean', + 'description' => __( 'Whether or not the marketing channel is set up.', 'woocommerce' ), + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'settings_url' => [ + 'description' => __( 'URL to the settings page, or the link to complete the setup/onboarding if the channel has not been set up yet.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'product_listings_status' => [ + 'description' => __( 'Status of the marketing channel\'s product listings.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'errors_count' => [ + 'description' => __( 'Number of channel issues/errors (e.g. account-related errors, product synchronization issues, etc.).', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } +} From 200156d7359960476e0df0e3477bae95f1439092 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:26:52 +0000 Subject: [PATCH 056/343] Add unit tests for `marketing/channels` API --- .../src/Admin/API/MarketingChannelsTest.php | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php new file mode 100644 index 00000000000..a043e77c753 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php @@ -0,0 +1,69 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + /** + * MarketingChannels class. + * + * @var MarketingChannelsService $marketing_channels_service + */ + $this->marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + } + + /** + * Tests that the registered marketing channels are returned by the endpoint. + */ + public function test_returns_registered_marketing_channels() { + // Register marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + $test_channel_1->expects( $this->any() )->method( 'get_name' )->willReturn( 'Test Channel One' ); + $this->marketing_channels_service->register( $test_channel_1 ); + + $request = new WP_REST_Request( 'GET', self::ENDPOINT ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 1, $data ); + $this->assertEquals( 'test-channel-1', $data[0]['slug'] ); + $this->assertEquals( 'Test Channel One', $data[0]['name'] ); + } + +} From 817ca2a96fbb902ed9612c083e80201c9be51eb0 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:27:10 +0000 Subject: [PATCH 057/343] Add `marketing/campaigns` API --- plugins/woocommerce/src/Admin/API/Init.php | 1 + .../src/Admin/API/MarketingCampaigns.php | 237 ++++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 plugins/woocommerce/src/Admin/API/MarketingCampaigns.php diff --git a/plugins/woocommerce/src/Admin/API/Init.php b/plugins/woocommerce/src/Admin/API/Init.php index bf2232185ca..8fff86a1ee1 100644 --- a/plugins/woocommerce/src/Admin/API/Init.php +++ b/plugins/woocommerce/src/Admin/API/Init.php @@ -66,6 +66,7 @@ class Init { 'Automattic\WooCommerce\Admin\API\MarketingOverview', 'Automattic\WooCommerce\Admin\API\MarketingRecommendations', 'Automattic\WooCommerce\Admin\API\MarketingChannels', + 'Automattic\WooCommerce\Admin\API\MarketingCampaigns', 'Automattic\WooCommerce\Admin\API\Options', 'Automattic\WooCommerce\Admin\API\Orders', 'Automattic\WooCommerce\Admin\API\PaymentGatewaySuggestions', diff --git a/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php new file mode 100644 index 00000000000..9c479d5e705 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php @@ -0,0 +1,237 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to view marketing campaigns. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'install_plugins' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing campaigns.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + return true; + } + + + /** + * Returns an aggregated array of marketing campaigns for all active marketing channels. + * + * @param WP_REST_Request $request Request data. + * + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + /** + * MarketingChannels class. + * + * @var MarketingChannelsService $marketing_channels_service + */ + $marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + + // Aggregate the campaigns from all registered marketing channels. + $responses = []; + foreach ( $marketing_channels_service->get_registered_channels() as $channel ) { + foreach ( $channel->get_campaigns() as $campaign ) { + $response = $this->prepare_item_for_response( $campaign, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + } + + // Pagination. + $page = $request['page']; + $items_per_page = $request['per_page']; + $offset = ( $page - 1 ) * $items_per_page; + $paginated_results = array_slice( $responses, $offset, $items_per_page ); + + $response = rest_ensure_response( $paginated_results ); + + $total_campaigns = count( $responses ); + $max_pages = ceil( $total_campaigns / $items_per_page ); + $response->header( 'X-WP-Total', $total_campaigns ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + // Add previous and next page links to response header. + $request_params = $request->get_query_params(); + $base = add_query_arg( urlencode_deep( $request_params ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Prepares the item for the REST response. + * + * @param MarketingCampaign $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $data = [ + 'id' => $item->get_id(), + 'channel' => $item->get_channel()->get_slug(), + 'title' => $item->get_title(), + 'manage_url' => $item->get_manage_url(), + ]; + + if ( $item->get_cost() instanceof Price ) { + $data['cost'] = [ + 'value' => wc_format_decimal( $item->get_cost()->get_value() ), + 'currency' => $item->get_cost()->get_currency(), + ]; + } + + $context = $request['context'] ?? 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_campaign', + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'description' => __( 'The unique identifier for the marketing campaign.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'channel' => [ + 'description' => __( 'The unique identifier for the marketing channel that this campaign belongs to.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'title' => [ + 'description' => __( 'Title of the marketing campaign.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'manage_url' => [ + 'description' => __( 'URL to the campaign management page', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'cost' => [ + 'description' => __( 'Cost of the marketing campaign.', 'woocommerce' ), + 'context' => [ 'view' ], + 'readonly' => true, + 'type' => 'object', + 'properties' => [ + 'value' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'currency' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Retrieves the query params for the collections. + * + * @return array Query parameters for the collection. + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + unset( $params['search'] ); + + return $params; + } + + +} From e82feb21da36988562c67b54e85a8c099e19c69b Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:27:19 +0000 Subject: [PATCH 058/343] Add unit tests for `marketing/campaigns` API --- .../src/Admin/API/MarketingCampaignsTest.php | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php new file mode 100644 index 00000000000..27b94bc4239 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php @@ -0,0 +1,140 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + /** + * MarketingChannels class. + * + * @var MarketingChannelsService $marketing_channels_service + */ + $this->marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + } + + /** + * Tests that the marketing campaigns for all registered channels are aggregated and returned by the endpoint. + */ + public function test_returns_aggregated_marketing_campaigns() { + // Create a mock marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + // Create a mock marketing campaign. + $test_campaign_1 = $this->createMock( MarketingCampaign::class ); + $test_campaign_1->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-1' ); + $test_campaign_1->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_1 ); + // Return the sample campaign by the mock marketing channel. + $test_channel_1->expects( $this->any() )->method( 'get_campaigns' )->willReturn( [ $test_campaign_1 ] ); + // Register the marketing channel. + $this->marketing_channels_service->register( $test_channel_1 ); + + // Create a second mock marketing channel. + $test_channel_2 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_2->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-2' ); + // Create a mock marketing campaign for the second marketing channel. + $test_campaign_2 = $this->createMock( MarketingCampaign::class ); + $test_campaign_2->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-2' ); + $test_campaign_2->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_2 ); + // Return the sample campaign by the second mock marketing channel. + $test_channel_2->expects( $this->any() )->method( 'get_campaigns' )->willReturn( [ $test_campaign_2 ] ); + // Register the second marketing channel. + $this->marketing_channels_service->register( $test_channel_2 ); + + $request = new WP_REST_Request( 'GET', self::ENDPOINT ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 2, $data ); + $this->assertEquals( + [ + 'test-campaign-1', + 'test-campaign-2', + ], + array_column( $data, 'id' ) + ); + $this->assertEquals( + [ + 'test-channel-1', + 'test-channel-2', + ], + array_column( $data, 'channel' ) + ); + } + + /** + * Tests that the marketing campaigns are paginated and then returned by the endpoint. + */ + public function test_paginates_marketing_campaigns() { + // Create a mock marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + // Return mock campaigns by the mock marketing channel. + $test_channel_1->expects( $this->any() )->method( 'get_campaigns' )->willReturn( + [ + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + ] + ); + // Register the marketing channel. + $this->marketing_channels_service->register( $test_channel_1 ); + + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( + [ + 'page' => '1', + 'per_page' => '2', + ] + ); + $response = $this->server->dispatch( $request ); + $headers = $response->get_headers(); + + $this->assertCount( 2, $response->get_data() ); + + $this->assertArrayHasKey( 'Link', $headers ); + $this->assertArrayHasKey( 'X-WP-Total', $headers ); + $this->assertArrayHasKey( 'X-WP-TotalPages', $headers ); + $this->assertEquals( 5, $headers['X-WP-Total'] ); + $this->assertEquals( 3, $headers['X-WP-TotalPages'] ); + } + +} From 44042634e6ab2a78447b22120cebbf5cdc23e5de Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:28:43 +0000 Subject: [PATCH 059/343] Translate Exception message --- plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php index a5fde99eb32..f7b96516753 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php @@ -33,7 +33,7 @@ class MarketingChannels { */ public function register( MarketingChannelInterface $channel ): void { if ( isset( $this->registered_channels[ $channel->get_slug() ] ) ) { - throw new Exception( 'Marketing channel cannot be registered because there is already a channel registered with the same slug!' ); + throw new Exception( __( 'Marketing channel cannot be registered because there is already a channel registered with the same slug!', 'woocommerce' ) ); } $this->registered_channels[ $channel->get_slug() ] = $channel; From f7be32dc9b4f1107edbb3bd115e04533dd075bed Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:30:48 +0000 Subject: [PATCH 060/343] Remove doc references to predetermined list of marketing channels --- .../woocommerce/src/Admin/Marketing/MarketingChannels.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php index f7b96516753..786aeb33b5f 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php @@ -23,8 +23,6 @@ class MarketingChannels { /** * 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 @@ -48,9 +46,6 @@ class MarketingChannels { /** * 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 From 3fb90016dc2b6e11d2e86e1387c5664304993fde Mon Sep 17 00:00:00 2001 From: Nima Karimi <73110514+nima-karimi@users.noreply.github.com> Date: Wed, 28 Dec 2022 17:25:37 +0000 Subject: [PATCH 061/343] Multichannel Marketing - Changes to the marketing classes (#36012) * Rename `get_errors_no` to `get_errors_count` * Remove the validation for marketing channel slugs Do not check if the marketing channel's slug exists in the list returned by WooCommerce.com Recommendation API. This allows any third-party extension to register as a marketing channel. * Revert InstalledExtensions The InstalledExtensions class will be used by the previous generation of the Marketing dashboard (if the user has not enabled the new "Marketing" feature); therefore, it's best to restore it to the original code. * Fix code style * Translate Exception message * Remove doc references to a predetermined list of marketing channels Co-authored-by: Nima --- .../src/Admin/API/MarketingOverview.php | 9 +- .../Admin/Marketing/InstalledExtensions.php | 617 +++++++++++++++++- .../Marketing/MarketingChannelInterface.php | 2 +- .../src/Admin/Marketing/MarketingChannels.php | 84 +-- .../src/Internal/Admin/Marketing.php | 9 +- .../MarketingServiceProvider.php | 5 +- .../Admin/Marketing/MarketingChannelsTest.php | 79 +-- 7 files changed, 624 insertions(+), 181 deletions(-) diff --git a/plugins/woocommerce/src/Admin/API/MarketingOverview.php b/plugins/woocommerce/src/Admin/API/MarketingOverview.php index 883ce04c1bb..930dcb4c0fc 100644 --- a/plugins/woocommerce/src/Admin/API/MarketingOverview.php +++ b/plugins/woocommerce/src/Admin/API/MarketingOverview.php @@ -125,14 +125,7 @@ class MarketingOverview extends \WC_REST_Data_Controller { * @return \WP_Error|\WP_REST_Response */ public function get_installed_plugins( $request ) { - /** - * InstalledExtensions - * - * @var InstalledExtensions $installed_extensions - */ - $installed_extensions = wc_get_container()->get( InstalledExtensions::class ); - - return rest_ensure_response( $installed_extensions->get_data() ); + return rest_ensure_response( InstalledExtensions::get_data() ); } } diff --git a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php index 9669b9014c6..b1e9ad82b80 100644 --- a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php +++ b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php @@ -5,46 +5,597 @@ 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 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() - ); + 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; } + + /** + * 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 ( 'activated' === $data['status'] && 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'; + + $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/MarketingChannelInterface.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php index 3a7233f073b..b2dbc3819a2 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php @@ -71,7 +71,7 @@ interface MarketingChannelInterface { * * @return int The number of issues to resolve, or 0 if there are no issues with the channel. */ - public function get_errors_no(): int; + public function get_errors_count(): int; /** * Returns an array of the channel's marketing campaigns. diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php index 0fcb6fd1e5e..786aeb33b5f 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php @@ -5,7 +5,7 @@ namespace Automattic\WooCommerce\Admin\Marketing; -use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs; +use Exception; /** * MarketingChannels repository class @@ -20,49 +20,18 @@ class MarketingChannels { */ private $registered_channels = []; - /** - * Array of plugin slugs for allowed marketing channels. - * - * @var string[] - */ - private $allowed_channels; - - /** - * MarketingSpecs repository - * - * @var MarketingSpecs - */ - protected $marketing_specs; - - /** - * Class initialization, invoked by the DI container. - * - * @param MarketingSpecs $marketing_specs The MarketingSpecs class. - * - * @internal - */ - final public function init( MarketingSpecs $marketing_specs ) { - $this->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. + * @throws Exception If the given marketing channel is already registered. */ 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; + if ( isset( $this->registered_channels[ $channel->get_slug() ] ) ) { + throw new Exception( __( 'Marketing channel cannot be registered because there is already a channel registered with the same slug!', 'woocommerce' ) ); } $this->registered_channels[ $channel->get_slug() ] = $channel; @@ -77,55 +46,12 @@ class MarketingChannels { /** * 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() ] ); + return array_values( $channels ); } } diff --git a/plugins/woocommerce/src/Internal/Admin/Marketing.php b/plugins/woocommerce/src/Internal/Admin/Marketing.php index f11cd2d31d9..47d4ab241bb 100644 --- a/plugins/woocommerce/src/Internal/Admin/Marketing.php +++ b/plugins/woocommerce/src/Internal/Admin/Marketing.php @@ -165,14 +165,7 @@ class Marketing { return $settings; } - /** - * InstalledExtensions helper class. - * - * @var InstalledExtensions $installed_extensions - */ - $installed_extensions = wc_get_container()->get( InstalledExtensions::class ); - - $settings['marketing']['installedExtensions'] = $installed_extensions->get_data(); + $settings['marketing']['installedExtensions'] = InstalledExtensions::get_data(); return $settings; } diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php index 8e27386ae86..c00e484fe89 100644 --- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php @@ -5,7 +5,6 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders; -use Automattic\WooCommerce\Admin\Marketing\InstalledExtensions; use Automattic\WooCommerce\Admin\Marketing\MarketingChannels; use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs; use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider; @@ -30,7 +29,6 @@ class MarketingServiceProvider extends AbstractServiceProvider { protected $provides = array( MarketingSpecs::class, MarketingChannels::class, - InstalledExtensions::class, ); /** @@ -38,7 +36,6 @@ class MarketingServiceProvider extends AbstractServiceProvider { */ public function register() { $this->share( MarketingSpecs::class ); - $this->share( MarketingChannels::class )->addArgument( MarketingSpecs::class ); - $this->share( InstalledExtensions::class )->addArgument( MarketingChannels::class ); + $this->share( MarketingChannels::class ); } } diff --git a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php index cf5885a0557..fdc112ae370 100644 --- a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php +++ b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php @@ -4,7 +4,6 @@ namespace Automattic\WooCommerce\Tests\Admin\Marketing; use Automattic\WooCommerce\Admin\Marketing\MarketingChannelInterface; use Automattic\WooCommerce\Admin\Marketing\MarketingChannels; -use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs; use WC_Unit_Test_Case; /** @@ -16,29 +15,17 @@ class MarketingChannelsTest extends WC_Unit_Test_Case { * Runs before each test. */ public function setUp(): void { - delete_transient( MarketingSpecs::RECOMMENDED_PLUGINS_TRANSIENT ); + remove_all_filters( 'woocommerce_marketing_channels' ); } /** - * @testdox A marketing channel can be registered using the `register` method if it is in the allowed list. + * @testdox A marketing channel can be registered using the `register` method if the same channel slug is NOT previously registered. */ - public function test_registers_allowed_channels() { + public function test_registers_channel() { $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 ); $marketing_channels->register( $test_channel ); $this->assertNotEmpty( $marketing_channels->get_registered_channels() ); @@ -46,42 +33,33 @@ class MarketingChannelsTest extends WC_Unit_Test_Case { } /** - * @testdox A marketing channel can NOT be registered using the `register` method if it is NOT in the allowed list. + * @testdox A marketing channel can NOT be registered using the `register` method if it is previously registered. */ - 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' ); + public function test_throws_exception_if_registering_existing_channels() { + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->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( [] ); + $test_channel_1_duplicate = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1_duplicate->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); $marketing_channels = new MarketingChannels(); - $marketing_channels->init( $marketing_specs ); - $marketing_channels->register( $test_channel ); + $marketing_channels->register( $test_channel_1 ); - $this->assertEmpty( $marketing_channels->get_registered_channels() ); + $this->expectException( \Exception::class ); + $marketing_channels->register( $test_channel_1_duplicate ); + + $this->assertCount( 1, $marketing_channels->get_registered_channels() ); + $this->assertEquals( $test_channel_1, $marketing_channels->get_registered_channels()[0] ); } /** - * @testdox A marketing channel can be registered using the `woocommerce_marketing_channels` WordPress filter if it is in the allowed list. + * @testdox A marketing channel can be registered using the `woocommerce_marketing_channels` WordPress filter if the same channel slug is NOT previously registered. */ - public function test_registers_allowed_channels_using_wp_filter() { + public function test_registers_channel_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', @@ -97,24 +75,29 @@ class MarketingChannelsTest extends WC_Unit_Test_Case { } /** - * @testdox A marketing channel can NOT be registered using the `woocommerce_marketing_channels` WordPress filter if it NOT is in the allowed list. + * @testdox A marketing channel can NOT be registered using the `woocommerce_marketing_channels` WordPress filter if it is previously registered. */ - 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' ); + public function test_overrides_existing_channel_if_registered_using_wp_filter() { + $marketing_channels = new MarketingChannels(); - set_transient( MarketingSpecs::RECOMMENDED_PLUGINS_TRANSIENT, [] ); + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + + $marketing_channels->register( $test_channel_1 ); + + $test_channel_1_duplicate = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1_duplicate->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); add_filter( 'woocommerce_marketing_channels', - function ( array $channels ) use ( $test_channel ) { - $channels[ $test_channel->get_slug() ] = $test_channel; + function ( array $channels ) use ( $test_channel_1_duplicate ) { + $channels[ $test_channel_1_duplicate->get_slug() ] = $test_channel_1_duplicate; return $channels; } ); - $marketing_channels = new MarketingChannels(); - $this->assertEmpty( $marketing_channels->get_registered_channels() ); + $this->assertCount( 1, $marketing_channels->get_registered_channels() ); + $this->assertEquals( $test_channel_1_duplicate, $marketing_channels->get_registered_channels()[0] ); } } From 130b2a94c605295bf2988472b1d8421c4da5e792 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 21:05:09 +0000 Subject: [PATCH 062/343] Add `unregister_all` method To allow unregistering all marketing channels. --- .../src/Admin/Marketing/MarketingChannels.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php index 786aeb33b5f..61cce159181 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php @@ -37,6 +37,15 @@ class MarketingChannels { $this->registered_channels[ $channel->get_slug() ] = $channel; } + /** + * Unregisters all marketing channels. + * + * @return void + */ + public function unregister_all(): void { + unset( $this->registered_channels ); + } + /** * Returns an array of all registered marketing channels. * From 4d6ef8ece34218539f88f232352a4f3c4f9a4bb1 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 21:05:27 +0000 Subject: [PATCH 063/343] Unregister all channels on test tear down --- .../src/Admin/API/MarketingCampaignsTest.php | 18 +++++++++++++----- .../src/Admin/API/MarketingChannelsTest.php | 18 +++++++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php index 27b94bc4239..313fca64b66 100644 --- a/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php @@ -26,6 +26,11 @@ class MarketingCampaignsTest extends WC_REST_Unit_Test_Case { */ const ENDPOINT = '/wc-admin/marketing/campaigns'; + /** + * @var MarketingChannelsService + */ + private $marketing_channels_service; + /** * Set up. */ @@ -40,14 +45,17 @@ class MarketingCampaignsTest extends WC_REST_Unit_Test_Case { ); wp_set_current_user( $this->user ); - /** - * MarketingChannels class. - * - * @var MarketingChannelsService $marketing_channels_service - */ $this->marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); } + /** + * Test teardown. + */ + public function tearDown(): void { + $this->marketing_channels_service->unregister_all(); + parent::tearDown(); + } + /** * Tests that the marketing campaigns for all registered channels are aggregated and returned by the endpoint. */ diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php index a043e77c753..9159b977ba4 100644 --- a/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php @@ -25,6 +25,11 @@ class MarketingChannelsTest extends WC_REST_Unit_Test_Case { */ const ENDPOINT = '/wc-admin/marketing/channels'; + /** + * @var MarketingChannelsService + */ + private $marketing_channels_service; + /** * Set up. */ @@ -39,14 +44,17 @@ class MarketingChannelsTest extends WC_REST_Unit_Test_Case { ); wp_set_current_user( $this->user ); - /** - * MarketingChannels class. - * - * @var MarketingChannelsService $marketing_channels_service - */ $this->marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); } + /** + * Test teardown. + */ + public function tearDown(): void { + $this->marketing_channels_service->unregister_all(); + parent::tearDown(); + } + /** * Tests that the registered marketing channels are returned by the endpoint. */ From 264c92a52fef791bbd6ed43c7ede5ac1aeb1687e Mon Sep 17 00:00:00 2001 From: Nima Date: Tue, 3 Jan 2023 11:27:31 +0000 Subject: [PATCH 064/343] Change API access denied authorization code --- plugins/woocommerce/src/Admin/API/MarketingCampaigns.php | 4 ++-- plugins/woocommerce/src/Admin/API/MarketingChannels.php | 2 +- .../woocommerce/src/Admin/API/MarketingRecommendations.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php index 9c479d5e705..e59ede8740d 100644 --- a/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php +++ b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php @@ -68,7 +68,7 @@ class MarketingCampaigns extends WC_REST_Controller { */ public function get_items_permissions_check( $request ) { if ( ! current_user_can( 'install_plugins' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing campaigns.', 'woocommerce' ), array( 'status' => 400 ) ); + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing campaigns.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -192,7 +192,7 @@ class MarketingCampaigns extends WC_REST_Controller { 'readonly' => true, ], 'manage_url' => [ - 'description' => __( 'URL to the campaign management page', 'woocommerce' ), + 'description' => __( 'URL to the campaign management page.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view' ], 'readonly' => true, diff --git a/plugins/woocommerce/src/Admin/API/MarketingChannels.php b/plugins/woocommerce/src/Admin/API/MarketingChannels.php index a02039339f1..6772cfbcc42 100644 --- a/plugins/woocommerce/src/Admin/API/MarketingChannels.php +++ b/plugins/woocommerce/src/Admin/API/MarketingChannels.php @@ -66,7 +66,7 @@ class MarketingChannels extends WC_REST_Controller { */ public function get_items_permissions_check( $request ) { if ( ! current_user_can( 'install_plugins' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing channels.', 'woocommerce' ), array( 'status' => 400 ) ); + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing channels.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; diff --git a/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php b/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php index a64fcce02d7..7e9cbf398b8 100644 --- a/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php +++ b/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php @@ -74,7 +74,7 @@ class MarketingRecommendations extends WC_REST_Controller { */ public function get_items_permissions_check( $request ) { if ( ! current_user_can( 'install_plugins' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing channels.', 'woocommerce' ), array( 'status' => 400 ) ); + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing channels.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; From 20efcfb8042f725e57a6b8c868d5496549302d3d Mon Sep 17 00:00:00 2001 From: Nima Date: Tue, 3 Jan 2023 14:05:27 +0000 Subject: [PATCH 065/343] Change API access permission --- plugins/woocommerce/src/Admin/API/MarketingCampaigns.php | 4 ++-- plugins/woocommerce/src/Admin/API/MarketingChannels.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php index e59ede8740d..8316d9ef11f 100644 --- a/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php +++ b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php @@ -67,8 +67,8 @@ class MarketingCampaigns extends WC_REST_Controller { * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { - if ( ! current_user_can( 'install_plugins' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing campaigns.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; diff --git a/plugins/woocommerce/src/Admin/API/MarketingChannels.php b/plugins/woocommerce/src/Admin/API/MarketingChannels.php index 6772cfbcc42..bb22cd01c74 100644 --- a/plugins/woocommerce/src/Admin/API/MarketingChannels.php +++ b/plugins/woocommerce/src/Admin/API/MarketingChannels.php @@ -65,8 +65,8 @@ class MarketingChannels extends WC_REST_Controller { * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { - if ( ! current_user_can( 'install_plugins' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing channels.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; From 16874164138e6332af0960b59faafc04ec60ffd3 Mon Sep 17 00:00:00 2001 From: Nima Date: Tue, 3 Jan 2023 14:54:03 +0000 Subject: [PATCH 066/343] Add MarketingCampaignType class This allows defining campaign types for each marketing channel. --- .../Admin/Marketing/MarketingCampaignType.php | 130 ++++++++++++++++++ .../Marketing/MarketingChannelInterface.php | 7 + 2 files changed, 137 insertions(+) create mode 100644 plugins/woocommerce/src/Admin/Marketing/MarketingCampaignType.php diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaignType.php b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaignType.php new file mode 100644 index 00000000000..032dc6f249d --- /dev/null +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaignType.php @@ -0,0 +1,130 @@ +id = $id; + $this->channel = $channel; + $this->name = $name; + $this->description = $description; + $this->create_url = $create_url; + $this->icon_url = $icon_url; + } + + /** + * Returns the marketing campaign's unique identifier. + * + * @return string + */ + public function get_id(): string { + return $this->id; + } + + /** + * Returns the marketing channel that this campaign type belongs to. + * + * @return MarketingChannelInterface + */ + public function get_channel(): MarketingChannelInterface { + return $this->channel; + } + + /** + * Returns the name of the marketing campaign type. + * + * @return string + */ + public function get_name(): string { + return $this->name; + } + + /** + * Returns the description of the marketing campaign type. + * + * @return string + */ + public function get_description(): string { + return $this->description; + } + + /** + * Returns the URL to the create campaign page. + * + * @return string + */ + public function get_create_url(): string { + return $this->create_url; + } + + /** + * Returns the URL to an image/icon for the campaign type. + * + * @return string + */ + public function get_icon_url(): string { + return $this->icon_url; + } +} diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php index b2dbc3819a2..6794c4b4259 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php @@ -73,6 +73,13 @@ interface MarketingChannelInterface { */ public function get_errors_count(): int; + /** + * Returns an array of marketing campaign types that the channel supports. + * + * @return MarketingCampaignType[] Array of marketing campaign type objects. + */ + public function get_supported_campaign_types(): array; + /** * Returns an array of the channel's marketing campaigns. * From 8ef6532a078153e3bdb196f52cfa8eeb095a7b2b Mon Sep 17 00:00:00 2001 From: Nima Date: Tue, 3 Jan 2023 14:55:06 +0000 Subject: [PATCH 067/343] Add campaign type property to campaign class --- .../src/Admin/API/MarketingCampaigns.php | 2 +- .../src/Admin/Marketing/MarketingCampaign.php | 47 ++++++------------- .../Admin/Marketing/MarketingCampaignTest.php | 40 +++------------- 3 files changed, 23 insertions(+), 66 deletions(-) diff --git a/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php index 8316d9ef11f..d8f9ac79378 100644 --- a/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php +++ b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php @@ -143,7 +143,7 @@ class MarketingCampaigns extends WC_REST_Controller { public function prepare_item_for_response( $item, $request ) { $data = [ 'id' => $item->get_id(), - 'channel' => $item->get_channel()->get_slug(), + 'channel' => $item->get_type()->get_channel()->get_slug(), 'title' => $item->get_title(), 'manage_url' => $item->get_manage_url(), ]; diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php index 45037fea367..c85ced83f21 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php @@ -7,14 +7,12 @@ namespace Automattic\WooCommerce\Admin\Marketing; -use JsonSerializable; - /** * MarketingCampaign class * * @since x.x.x */ -class MarketingCampaign implements JsonSerializable { +class MarketingCampaign { /** * The unique identifier. * @@ -23,11 +21,11 @@ class MarketingCampaign implements JsonSerializable { protected $id; /** - * The marketing channel that this campaign belongs to. + * The marketing campaign type. * - * @var MarketingChannelInterface + * @var MarketingCampaignType */ - protected $channel; + protected $type; /** * Title of the marketing campaign. @@ -53,15 +51,15 @@ class MarketingCampaign implements JsonSerializable { /** * MarketingCampaign constructor. * - * @param string $id The marketing campaign's unique identifier. - * @param MarketingChannelInterface $channel The marketing channel that this campaign belongs to. - * @param string $title The title of the marketing campaign. - * @param string $manage_url The URL to the channel's campaign management page. - * @param Price|null $cost The cost of the marketing campaign with the currency. + * @param string $id The marketing campaign's unique identifier. + * @param MarketingCampaignType $type The marketing campaign type. + * @param string $title The title of the marketing campaign. + * @param string $manage_url The URL to the channel's campaign management page. + * @param Price|null $cost The cost of the marketing campaign with the currency. */ - public function __construct( string $id, MarketingChannelInterface $channel, string $title, string $manage_url, Price $cost = null ) { + public function __construct( string $id, MarketingCampaignType $type, string $title, string $manage_url, Price $cost = null ) { $this->id = $id; - $this->channel = $channel; + $this->type = $type; $this->title = $title; $this->manage_url = $manage_url; $this->cost = $cost; @@ -77,12 +75,12 @@ class MarketingCampaign implements JsonSerializable { } /** - * Returns the marketing channel that this campaign belongs to. + * Returns the marketing campaign type. * - * @return MarketingChannelInterface + * @return MarketingCampaignType */ - public function get_channel(): MarketingChannelInterface { - return $this->channel; + public function get_type(): MarketingCampaignType { + return $this->type; } /** @@ -111,19 +109,4 @@ class MarketingCampaign implements JsonSerializable { public function get_cost(): ?Price { return $this->cost; } - - /** - * Serialize the marketing campaign data. - * - * @return array - */ - public function jsonSerialize() { - return [ - 'id' => $this->get_id(), - 'channel' => $this->get_channel()->get_slug(), - 'title' => $this->get_title(), - 'manage_url' => $this->get_manage_url(), - 'cost' => $this->get_cost(), - ]; - } } diff --git a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php index 30d8c243ba5..4277cff9437 100644 --- a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php +++ b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php @@ -3,7 +3,7 @@ namespace Automattic\WooCommerce\Tests\Admin\Marketing; use Automattic\WooCommerce\Admin\Marketing\MarketingCampaign; -use Automattic\WooCommerce\Admin\Marketing\MarketingChannelInterface; +use Automattic\WooCommerce\Admin\Marketing\MarketingCampaignType; use Automattic\WooCommerce\Admin\Marketing\Price; use WC_Unit_Test_Case; @@ -13,15 +13,15 @@ use WC_Unit_Test_Case; class MarketingCampaignTest extends WC_Unit_Test_Case { /** - * @testdox `get_id`, `get_title`, `get_manage_url`, and `get_cost` return the class properties set by the constructor. + * @testdox `get_id`, `get_type`, `get_title`, `get_manage_url`, and `get_cost` return the class properties set by the constructor. */ public function test_get_methods_return_properties() { - $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); - $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + $test_campaign_type_1 = $this->createMock( MarketingCampaignType::class ); - $marketing_campaign = new MarketingCampaign( '1234', $test_channel_1, 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); + $marketing_campaign = new MarketingCampaign( '1234', $test_campaign_type_1, 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); $this->assertEquals( '1234', $marketing_campaign->get_id() ); + $this->assertEquals( $test_campaign_type_1, $marketing_campaign->get_type() ); $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() ); @@ -33,36 +33,10 @@ class MarketingCampaignTest extends WC_Unit_Test_Case { * @testdox `cost` property can be null. */ public function test_cost_can_be_null() { - $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_campaign_type_1 = $this->createMock( MarketingCampaignType::class ); - $marketing_campaign = new MarketingCampaign( '1234', $test_channel_1, 'Ad #1234', 'https://example.com/manage-campaigns' ); + $marketing_campaign = new MarketingCampaign( '1234', $test_campaign_type_1, '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() { - $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); - $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); - - $marketing_campaign = new MarketingCampaign( '1234', $test_channel_1, '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(), - 'channel' => 'test-channel-1', - '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 ) - ); - } } From 1b668d94f3d490f27ac8342ba2fa9b54bfd49415 Mon Sep 17 00:00:00 2001 From: Nima Date: Tue, 3 Jan 2023 14:55:52 +0000 Subject: [PATCH 068/343] Add `marketing/campaign-types` API This API returns the aggregated list of supported marketing campaign types for all registered marketing channels. --- plugins/woocommerce/src/Admin/API/Init.php | 1 + .../src/Admin/API/MarketingCampaignTypes.php | 211 ++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php diff --git a/plugins/woocommerce/src/Admin/API/Init.php b/plugins/woocommerce/src/Admin/API/Init.php index 8fff86a1ee1..625374b2683 100644 --- a/plugins/woocommerce/src/Admin/API/Init.php +++ b/plugins/woocommerce/src/Admin/API/Init.php @@ -67,6 +67,7 @@ class Init { 'Automattic\WooCommerce\Admin\API\MarketingRecommendations', 'Automattic\WooCommerce\Admin\API\MarketingChannels', 'Automattic\WooCommerce\Admin\API\MarketingCampaigns', + 'Automattic\WooCommerce\Admin\API\MarketingCampaignTypes', 'Automattic\WooCommerce\Admin\API\Options', 'Automattic\WooCommerce\Admin\API\Orders', 'Automattic\WooCommerce\Admin\API\PaymentGatewaySuggestions', diff --git a/plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php b/plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php new file mode 100644 index 00000000000..f01dba7a649 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php @@ -0,0 +1,211 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Retrieves the query params for the collections. + * + * @return array Query parameters for the collection. + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + unset( $params['search'] ); + + return $params; + } + + /** + * Check whether a given request has permission to view marketing campaigns. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Returns an aggregated array of marketing campaigns for all active marketing channels. + * + * @param WP_REST_Request $request Request data. + * + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + /** + * MarketingChannels class. + * + * @var MarketingChannelsService $marketing_channels_service + */ + $marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + + // Aggregate the supported campaign types from all registered marketing channels. + $responses = []; + foreach ( $marketing_channels_service->get_registered_channels() as $channel ) { + foreach ( $channel->get_supported_campaign_types() as $campaign_type ) { + $response = $this->prepare_item_for_response( $campaign_type, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + } + + return rest_ensure_response( $responses ); + } + + /** + * Prepares the item for the REST response. + * + * @param MarketingCampaignType $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $data = [ + 'id' => $item->get_id(), + 'name' => $item->get_name(), + 'description' => $item->get_description(), + 'channel' => [ + 'slug' => $item->get_channel()->get_slug(), + 'name' => $item->get_channel()->get_name(), + ], + 'create_url' => $item->get_create_url(), + 'icon_url' => $item->get_icon_url(), + ]; + + $context = $request['context'] ?? 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_campaign_type', + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'description' => __( 'The unique identifier for the marketing campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'description' => __( 'Name of the marketing campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'description' => [ + 'description' => __( 'Description of the marketing campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'channel' => [ + 'description' => __( 'The marketing channel that this campaign type belongs to.', 'woocommerce' ), + 'type' => 'object', + 'context' => [ 'view' ], + 'readonly' => true, + 'properties' => [ + 'slug' => [ + 'description' => __( 'The unique identifier of the marketing channel that this campaign type belongs to.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'description' => __( 'The name of the marketing channel that this campaign type belongs to.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + 'create_url' => [ + 'description' => __( 'URL to the create campaign page for this campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'icon_url' => [ + 'description' => __( 'URL to an image/icon for the campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } + + +} From 02cd258b44352bdfc2775ab6e17bd4c2b187a231 Mon Sep 17 00:00:00 2001 From: Nima Date: Tue, 3 Jan 2023 15:00:10 +0000 Subject: [PATCH 069/343] Add unit tests for `marketing/campaign-types` API --- .../Admin/API/MarketingCampaignTypesTest.php | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php new file mode 100644 index 00000000000..f38b49e1bb5 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php @@ -0,0 +1,101 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + $this->marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + } + + /** + * Test teardown. + */ + public function tearDown(): void { + $this->marketing_channels_service->unregister_all(); + parent::tearDown(); + } + + /** + * Tests that the marketing campaigns for all registered channels are aggregated and returned by the endpoint. + */ + public function test_returns_aggregated_marketing_campaigns() { + // Create a mock marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + // Create a mock marketing campaign type. + $test_campaign_type_1 = $this->createMock( MarketingCampaignType::class ); + $test_campaign_type_1->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-type-1' ); + $test_campaign_type_1->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_1 ); + // Return the sample campaign type by the mock marketing channel. + $test_channel_1->expects( $this->any() )->method( 'get_supported_campaign_types' )->willReturn( [ $test_campaign_type_1 ] ); + // Register the marketing channel. + $this->marketing_channels_service->register( $test_channel_1 ); + + // Create a second mock marketing channel. + $test_channel_2 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_2->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-2' ); + // Create a mock marketing campaign type for the second marketing channel. + $test_campaign_type_2 = $this->createMock( MarketingCampaignType::class ); + $test_campaign_type_2->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-type-2' ); + $test_campaign_type_2->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_2 ); + // Return the sample campaign by the second mock marketing channel. + $test_channel_2->expects( $this->any() )->method( 'get_supported_campaign_types' )->willReturn( [ $test_campaign_type_2 ] ); + // Register the second marketing channel. + $this->marketing_channels_service->register( $test_channel_2 ); + + $request = new WP_REST_Request( 'GET', self::ENDPOINT ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 2, $data ); + $this->assertEquals( + [ + 'test-campaign-type-1', + 'test-campaign-type-2', + ], + array_column( $data, 'id' ) + ); + } + +} From cb2048004cf1f2ac92b063b7f8283554567963a0 Mon Sep 17 00:00:00 2001 From: Saad Tarhi Date: Fri, 13 Jan 2023 16:02:28 +0100 Subject: [PATCH 070/343] Add the Cart & Checkout Blocks as a feature --- .../woocommerce/src/Internal/Features/FeaturesController.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php index bf2622df5c2..8a014af58a7 100644 --- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php +++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php @@ -105,6 +105,11 @@ class FeaturesController { 'description' => __( 'Enable the high performance order storage feature.', 'woocommerce' ), 'is_experimental' => true, ), + 'cart_checkout_blocks' => array( + 'name' => __( 'Cart & Checkout Blocks', 'woocommerce' ), + 'description' => __( 'Optimize for faster checkout', 'woocommerce' ), + 'is_experimental' => false, + ), ); $this->legacy_feature_ids = array( 'analytics', 'new_navigation' ); From c42fdb039c6f7b56ade6c91fbe133e9793e885ae Mon Sep 17 00:00:00 2001 From: Saad Tarhi Date: Fri, 13 Jan 2023 16:05:40 +0100 Subject: [PATCH 071/343] Prevent displaying UI setting of the C&C Blocks Currently, we don't desire displaying the C&C Blocks feature's settings. We may choose otherwise in the future, but for the time being adding the C&C Block to the list of features does the work! --- .../woocommerce/src/Internal/Features/FeaturesController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php index 8a014af58a7..44f84e1a77a 100644 --- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php +++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php @@ -540,7 +540,7 @@ class FeaturesController { return $features[ $feature_id ]['is_experimental']; } ); - $mature_feature_ids = array_diff( $feature_ids, $experimental_feature_ids ); + $mature_feature_ids = array_diff( $feature_ids, $experimental_feature_ids, [ 'cart_checkout_blocks' ] ); $feature_ids = array_merge( $mature_feature_ids, array( 'mature_features_end' ), $experimental_feature_ids ); foreach ( $feature_ids as $id ) { From 728ba16b350780577b5789ef1c48124736457805 Mon Sep 17 00:00:00 2001 From: Saad Tarhi Date: Fri, 13 Jan 2023 16:16:26 +0100 Subject: [PATCH 072/343] Add a change file --- ...d-36413-support-for-cart-checkout-in-declare-compatibility | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-36413-support-for-cart-checkout-in-declare-compatibility diff --git a/plugins/woocommerce/changelog/add-36413-support-for-cart-checkout-in-declare-compatibility b/plugins/woocommerce/changelog/add-36413-support-for-cart-checkout-in-declare-compatibility new file mode 100644 index 00000000000..7724faa14f5 --- /dev/null +++ b/plugins/woocommerce/changelog/add-36413-support-for-cart-checkout-in-declare-compatibility @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Add the support for the C&C Blocks in declaring compatibility feature From b2ff0ba1a2b8012b5664ffea8f8d73b3114c236b Mon Sep 17 00:00:00 2001 From: Nima Karimi <73110514+nima-karimi@users.noreply.github.com> Date: Fri, 13 Jan 2023 16:54:48 +0000 Subject: [PATCH 073/343] Multichannel Marketing - API (#36222) * Rename `get_errors_no` to `get_errors_count` * Remove the validation for marketing channel slugs Do not check if the marketing channel's slug exists in the list returned by WooCommerce.com Recommendation API. This essentially allows any third-party extension to register as a marketing channel. * Revert InstalledExtensions The InstalledExtensions class will be used by the previous generation of Marketing dashboard (if the user has not enabled the new "Marketing" feature); therefore, it's best to restore it to the original code. * Fix code style * Add channel property to MarketingCampaign * Add methods to filter the recommended marketing channels and extensions * Add `marketing/recommendations` API * Add unit tests for `marketing/recommendations` API * Add `marketing/channels` API * Add unit tests for `marketing/channels` API * Add `marketing/campaigns` API * Add unit tests for `marketing/campaigns` API * Translate Exception message * Remove doc references to predetermined list of marketing channels * Add `unregister_all` method To allow unregistering all marketing channels. * Unregister all channels on test tear down * Change API access denied authorization code * Change API access permission * Add MarketingCampaignType class This allows defining campaign types for each marketing channel. * Add campaign type property to campaign class * Add `marketing/campaign-types` API This API returns the aggregated list of supported marketing campaign types for all registered marketing channels. * Add unit tests for `marketing/campaign-types` API * Remove unused jsonSerialize method * Fix unit tests Co-authored-by: Nima --- plugins/woocommerce/src/Admin/API/Init.php | 4 + .../src/Admin/API/MarketingCampaignTypes.php | 211 ++++++++++++++++ .../src/Admin/API/MarketingCampaigns.php | 237 ++++++++++++++++++ .../src/Admin/API/MarketingChannels.php | 192 ++++++++++++++ .../Admin/API/MarketingRecommendations.php | 235 +++++++++++++++++ .../src/Admin/Marketing/MarketingCampaign.php | 46 ++-- .../Admin/Marketing/MarketingCampaignType.php | 130 ++++++++++ .../Marketing/MarketingChannelInterface.php | 7 + .../src/Admin/Marketing/MarketingChannels.php | 9 + .../woocommerce/src/Admin/Marketing/Price.php | 16 +- .../Admin/Marketing/MarketingSpecs.php | 72 ++++++ .../Admin/API/MarketingCampaignTypesTest.php | 101 ++++++++ .../src/Admin/API/MarketingCampaignsTest.php | 155 ++++++++++++ .../src/Admin/API/MarketingChannelsTest.php | 77 ++++++ .../API/MarketingRecommendationsTest.php | 138 ++++++++++ .../Admin/Marketing/MarketingCampaignTest.php | 34 +-- 16 files changed, 1602 insertions(+), 62 deletions(-) create mode 100644 plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php create mode 100644 plugins/woocommerce/src/Admin/API/MarketingCampaigns.php create mode 100644 plugins/woocommerce/src/Admin/API/MarketingChannels.php create mode 100644 plugins/woocommerce/src/Admin/API/MarketingRecommendations.php create mode 100644 plugins/woocommerce/src/Admin/Marketing/MarketingCampaignType.php create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php diff --git a/plugins/woocommerce/src/Admin/API/Init.php b/plugins/woocommerce/src/Admin/API/Init.php index 42b2bac82e7..625374b2683 100644 --- a/plugins/woocommerce/src/Admin/API/Init.php +++ b/plugins/woocommerce/src/Admin/API/Init.php @@ -64,6 +64,10 @@ class Init { 'Automattic\WooCommerce\Admin\API\Experiments', 'Automattic\WooCommerce\Admin\API\Marketing', 'Automattic\WooCommerce\Admin\API\MarketingOverview', + 'Automattic\WooCommerce\Admin\API\MarketingRecommendations', + 'Automattic\WooCommerce\Admin\API\MarketingChannels', + 'Automattic\WooCommerce\Admin\API\MarketingCampaigns', + 'Automattic\WooCommerce\Admin\API\MarketingCampaignTypes', 'Automattic\WooCommerce\Admin\API\Options', 'Automattic\WooCommerce\Admin\API\Orders', 'Automattic\WooCommerce\Admin\API\PaymentGatewaySuggestions', diff --git a/plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php b/plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php new file mode 100644 index 00000000000..f01dba7a649 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php @@ -0,0 +1,211 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Retrieves the query params for the collections. + * + * @return array Query parameters for the collection. + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + unset( $params['search'] ); + + return $params; + } + + /** + * Check whether a given request has permission to view marketing campaigns. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Returns an aggregated array of marketing campaigns for all active marketing channels. + * + * @param WP_REST_Request $request Request data. + * + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + /** + * MarketingChannels class. + * + * @var MarketingChannelsService $marketing_channels_service + */ + $marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + + // Aggregate the supported campaign types from all registered marketing channels. + $responses = []; + foreach ( $marketing_channels_service->get_registered_channels() as $channel ) { + foreach ( $channel->get_supported_campaign_types() as $campaign_type ) { + $response = $this->prepare_item_for_response( $campaign_type, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + } + + return rest_ensure_response( $responses ); + } + + /** + * Prepares the item for the REST response. + * + * @param MarketingCampaignType $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $data = [ + 'id' => $item->get_id(), + 'name' => $item->get_name(), + 'description' => $item->get_description(), + 'channel' => [ + 'slug' => $item->get_channel()->get_slug(), + 'name' => $item->get_channel()->get_name(), + ], + 'create_url' => $item->get_create_url(), + 'icon_url' => $item->get_icon_url(), + ]; + + $context = $request['context'] ?? 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_campaign_type', + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'description' => __( 'The unique identifier for the marketing campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'description' => __( 'Name of the marketing campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'description' => [ + 'description' => __( 'Description of the marketing campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'channel' => [ + 'description' => __( 'The marketing channel that this campaign type belongs to.', 'woocommerce' ), + 'type' => 'object', + 'context' => [ 'view' ], + 'readonly' => true, + 'properties' => [ + 'slug' => [ + 'description' => __( 'The unique identifier of the marketing channel that this campaign type belongs to.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'description' => __( 'The name of the marketing channel that this campaign type belongs to.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + 'create_url' => [ + 'description' => __( 'URL to the create campaign page for this campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'icon_url' => [ + 'description' => __( 'URL to an image/icon for the campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } + + +} diff --git a/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php new file mode 100644 index 00000000000..d8f9ac79378 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php @@ -0,0 +1,237 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to view marketing campaigns. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + + /** + * Returns an aggregated array of marketing campaigns for all active marketing channels. + * + * @param WP_REST_Request $request Request data. + * + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + /** + * MarketingChannels class. + * + * @var MarketingChannelsService $marketing_channels_service + */ + $marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + + // Aggregate the campaigns from all registered marketing channels. + $responses = []; + foreach ( $marketing_channels_service->get_registered_channels() as $channel ) { + foreach ( $channel->get_campaigns() as $campaign ) { + $response = $this->prepare_item_for_response( $campaign, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + } + + // Pagination. + $page = $request['page']; + $items_per_page = $request['per_page']; + $offset = ( $page - 1 ) * $items_per_page; + $paginated_results = array_slice( $responses, $offset, $items_per_page ); + + $response = rest_ensure_response( $paginated_results ); + + $total_campaigns = count( $responses ); + $max_pages = ceil( $total_campaigns / $items_per_page ); + $response->header( 'X-WP-Total', $total_campaigns ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + // Add previous and next page links to response header. + $request_params = $request->get_query_params(); + $base = add_query_arg( urlencode_deep( $request_params ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Prepares the item for the REST response. + * + * @param MarketingCampaign $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $data = [ + 'id' => $item->get_id(), + 'channel' => $item->get_type()->get_channel()->get_slug(), + 'title' => $item->get_title(), + 'manage_url' => $item->get_manage_url(), + ]; + + if ( $item->get_cost() instanceof Price ) { + $data['cost'] = [ + 'value' => wc_format_decimal( $item->get_cost()->get_value() ), + 'currency' => $item->get_cost()->get_currency(), + ]; + } + + $context = $request['context'] ?? 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_campaign', + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'description' => __( 'The unique identifier for the marketing campaign.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'channel' => [ + 'description' => __( 'The unique identifier for the marketing channel that this campaign belongs to.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'title' => [ + 'description' => __( 'Title of the marketing campaign.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'manage_url' => [ + 'description' => __( 'URL to the campaign management page.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'cost' => [ + 'description' => __( 'Cost of the marketing campaign.', 'woocommerce' ), + 'context' => [ 'view' ], + 'readonly' => true, + 'type' => 'object', + 'properties' => [ + 'value' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'currency' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Retrieves the query params for the collections. + * + * @return array Query parameters for the collection. + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + unset( $params['search'] ); + + return $params; + } + + +} diff --git a/plugins/woocommerce/src/Admin/API/MarketingChannels.php b/plugins/woocommerce/src/Admin/API/MarketingChannels.php new file mode 100644 index 00000000000..bb22cd01c74 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingChannels.php @@ -0,0 +1,192 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to view marketing channels. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Return installed marketing channels. + * + * @param WP_REST_Request $request Request data. + * + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + /** + * MarketingChannels class. + * + * @var MarketingChannelsService $marketing_channels_service + */ + $marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + + $channels = $marketing_channels_service->get_registered_channels(); + + $responses = []; + foreach ( $channels as $item ) { + $response = $this->prepare_item_for_response( $item, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $responses ); + } + + /** + * Prepares the item for the REST response. + * + * @param MarketingChannelInterface $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $data = [ + 'slug' => $item->get_slug(), + 'is_setup_completed' => $item->is_setup_completed(), + 'settings_url' => $item->get_setup_url(), + 'name' => $item->get_name(), + 'description' => $item->get_description(), + 'product_listings_status' => $item->get_product_listings_status(), + 'errors_count' => $item->get_errors_count(), + 'icon' => $item->get_icon_url(), + ]; + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_channel', + 'type' => 'object', + 'properties' => [ + 'slug' => [ + 'description' => __( 'Unique identifier string for the marketing channel extension, also known as the plugin slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'description' => __( 'Name of the marketing channel.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'description' => [ + 'description' => __( 'Description of the marketing channel.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'icon' => [ + 'description' => __( 'Path to the channel icon.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'is_setup_completed' => [ + 'type' => 'boolean', + 'description' => __( 'Whether or not the marketing channel is set up.', 'woocommerce' ), + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'settings_url' => [ + 'description' => __( 'URL to the settings page, or the link to complete the setup/onboarding if the channel has not been set up yet.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'product_listings_status' => [ + 'description' => __( 'Status of the marketing channel\'s product listings.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'errors_count' => [ + 'description' => __( 'Number of channel issues/errors (e.g. account-related errors, product synchronization issues, etc.).', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php b/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php new file mode 100644 index 00000000000..7e9cbf398b8 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php @@ -0,0 +1,235 @@ +namespace, + '/' . $this->rest_base, + [ + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_items' ], + 'permission_callback' => [ $this, 'get_items_permissions_check' ], + 'args' => [ + 'category' => [ + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + 'sanitize_callback' => 'sanitize_title_with_dashes', + 'enum' => [ 'channels', 'extensions' ], + 'required' => true, + ], + ], + ], + 'schema' => [ $this, 'get_public_item_schema' ], + ] + ); + } + + /** + * Check whether a given request has permission to view marketing recommendations. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'install_plugins' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing channels.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Retrieves a collection of recommendations. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + /** + * MarketingSpecs class. + * + * @var MarketingSpecs $marketing_specs + */ + $marketing_specs = wc_get_container()->get( MarketingSpecs::class ); + + $category = $request->get_param( 'category' ); + if ( 'channels' === $category ) { + $items = $marketing_specs->get_recommended_marketing_channels(); + } elseif ( 'extensions' === $category ) { + $items = $marketing_specs->get_recommended_marketing_extensions_excluding_channels(); + } else { + return new WP_Error( 'woocommerce_rest_invalid_category', __( 'The specified category for recommendations is invalid. Allowed values: "channels", "extensions".', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $responses = []; + foreach ( $items as $item ) { + $response = $this->prepare_item_for_response( $item, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $responses ); + } + + /** + * Prepares the item for the REST response. + * + * @param array $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_recommendation', + 'type' => 'object', + 'properties' => [ + 'title' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'description' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'url' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'direct_install' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'icon' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'product' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'plugin' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'categories' => [ + 'type' => 'array', + 'context' => [ 'view' ], + 'readonly' => true, + 'items' => [ + 'type' => 'string', + ], + ], + 'subcategories' => [ + 'type' => 'array', + 'context' => [ 'view' ], + 'readonly' => true, + 'items' => [ + 'type' => 'object', + 'context' => [ 'view' ], + 'readonly' => true, + 'properties' => [ + 'slug' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + ], + 'tags' => [ + 'type' => 'array', + 'context' => [ 'view' ], + 'readonly' => true, + 'items' => [ + 'type' => 'object', + 'context' => [ 'view' ], + 'readonly' => true, + 'properties' => [ + 'slug' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php index 7b3f99a4b3a..c85ced83f21 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php @@ -7,14 +7,12 @@ namespace Automattic\WooCommerce\Admin\Marketing; -use JsonSerializable; - /** * MarketingCampaign class * * @since x.x.x */ -class MarketingCampaign implements JsonSerializable { +class MarketingCampaign { /** * The unique identifier. * @@ -22,6 +20,13 @@ class MarketingCampaign implements JsonSerializable { */ protected $id; + /** + * The marketing campaign type. + * + * @var MarketingCampaignType + */ + protected $type; + /** * Title of the marketing campaign. * @@ -46,13 +51,15 @@ class MarketingCampaign implements JsonSerializable { /** * MarketingCampaign constructor. * - * @param string $id The marketing campaign's unique identifier. - * @param string $title The title of the marketing campaign. - * @param string $manage_url The URL to the channel's campaign management page. - * @param Price|null $cost The cost of the marketing campaign with the currency. + * @param string $id The marketing campaign's unique identifier. + * @param MarketingCampaignType $type The marketing campaign type. + * @param string $title The title of the marketing campaign. + * @param string $manage_url The URL to the channel's campaign management page. + * @param Price|null $cost The cost of the marketing campaign with the currency. */ - public function __construct( string $id, string $title, string $manage_url, Price $cost = null ) { + public function __construct( string $id, MarketingCampaignType $type, string $title, string $manage_url, Price $cost = null ) { $this->id = $id; + $this->type = $type; $this->title = $title; $this->manage_url = $manage_url; $this->cost = $cost; @@ -67,6 +74,15 @@ class MarketingCampaign implements JsonSerializable { return $this->id; } + /** + * Returns the marketing campaign type. + * + * @return MarketingCampaignType + */ + public function get_type(): MarketingCampaignType { + return $this->type; + } + /** * Returns the title of the marketing campaign. * @@ -93,18 +109,4 @@ class MarketingCampaign implements JsonSerializable { 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/MarketingCampaignType.php b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaignType.php new file mode 100644 index 00000000000..032dc6f249d --- /dev/null +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaignType.php @@ -0,0 +1,130 @@ +id = $id; + $this->channel = $channel; + $this->name = $name; + $this->description = $description; + $this->create_url = $create_url; + $this->icon_url = $icon_url; + } + + /** + * Returns the marketing campaign's unique identifier. + * + * @return string + */ + public function get_id(): string { + return $this->id; + } + + /** + * Returns the marketing channel that this campaign type belongs to. + * + * @return MarketingChannelInterface + */ + public function get_channel(): MarketingChannelInterface { + return $this->channel; + } + + /** + * Returns the name of the marketing campaign type. + * + * @return string + */ + public function get_name(): string { + return $this->name; + } + + /** + * Returns the description of the marketing campaign type. + * + * @return string + */ + public function get_description(): string { + return $this->description; + } + + /** + * Returns the URL to the create campaign page. + * + * @return string + */ + public function get_create_url(): string { + return $this->create_url; + } + + /** + * Returns the URL to an image/icon for the campaign type. + * + * @return string + */ + public function get_icon_url(): string { + return $this->icon_url; + } +} diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php index b2dbc3819a2..6794c4b4259 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php @@ -73,6 +73,13 @@ interface MarketingChannelInterface { */ public function get_errors_count(): int; + /** + * Returns an array of marketing campaign types that the channel supports. + * + * @return MarketingCampaignType[] Array of marketing campaign type objects. + */ + public function get_supported_campaign_types(): array; + /** * Returns an array of the channel's marketing campaigns. * diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php index 786aeb33b5f..61cce159181 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php @@ -37,6 +37,15 @@ class MarketingChannels { $this->registered_channels[ $channel->get_slug() ] = $channel; } + /** + * Unregisters all marketing channels. + * + * @return void + */ + public function unregister_all(): void { + unset( $this->registered_channels ); + } + /** * Returns an array of all registered marketing channels. * diff --git a/plugins/woocommerce/src/Admin/Marketing/Price.php b/plugins/woocommerce/src/Admin/Marketing/Price.php index 9dbb00837ae..961228d5730 100644 --- a/plugins/woocommerce/src/Admin/Marketing/Price.php +++ b/plugins/woocommerce/src/Admin/Marketing/Price.php @@ -5,14 +5,12 @@ namespace Automattic\WooCommerce\Admin\Marketing; -use JsonSerializable; - /** * Price class * * @since x.x.x */ -class Price implements JsonSerializable { +class Price { /** * The price. * @@ -55,16 +53,4 @@ class Price implements JsonSerializable { 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/Internal/Admin/Marketing/MarketingSpecs.php b/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php index ee36ba1375c..e6ed4e875d9 100644 --- a/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php +++ b/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php @@ -28,6 +28,20 @@ class MarketingSpecs { */ const KNOWLEDGE_BASE_TRANSIENT = 'wc_marketing_knowledge_base'; + /** + * Slug of the category specifying marketing extensions on the WooCommerce.com store. + * + * @var string + */ + const MARKETING_EXTENSION_CATEGORY_SLUG = 'marketing'; + + /** + * Slug of the subcategory specifying marketing channels on the WooCommerce.com store. + * + * @var string + */ + const MARKETING_CHANNEL_SUBCATEGORY_SLUG = 'sales-channels'; + /** * Load recommended plugins from WooCommerce.com * @@ -61,6 +75,64 @@ class MarketingSpecs { return array_values( $plugins ); } + /** + * Return only the recommended marketing channels from WooCommerce.com. + * + * @return array + */ + public function get_recommended_marketing_channels(): array { + return array_filter( $this->get_recommended_plugins(), [ $this, 'is_marketing_channel_plugin' ] ); + } + + /** + * Return all recommended marketing extensions EXCEPT the marketing channels from WooCommerce.com. + * + * @return array + */ + public function get_recommended_marketing_extensions_excluding_channels(): array { + return array_filter( + $this->get_recommended_plugins(), + function ( array $plugin_data ) { + return $this->is_marketing_plugin( $plugin_data ) && ! $this->is_marketing_channel_plugin( $plugin_data ); + } + ); + } + + /** + * Returns whether a plugin is a marketing extension. + * + * @param array $plugin_data The plugin properties returned by the API. + * + * @return bool + */ + protected function is_marketing_plugin( array $plugin_data ): bool { + $categories = $plugin_data['categories'] ?? []; + + return in_array( self::MARKETING_EXTENSION_CATEGORY_SLUG, $categories, true ); + } + + /** + * Returns whether a plugin is a marketing channel. + * + * @param array $plugin_data The plugin properties returned by the API. + * + * @return bool + */ + protected function is_marketing_channel_plugin( array $plugin_data ): bool { + if ( ! $this->is_marketing_plugin( $plugin_data ) ) { + return false; + } + + $subcategories = $plugin_data['subcategories'] ?? []; + foreach ( $subcategories as $subcategory ) { + if ( isset( $subcategory['slug'] ) && self::MARKETING_CHANNEL_SUBCATEGORY_SLUG === $subcategory['slug'] ) { + return true; + } + } + + return false; + } + /** * Load knowledge base posts from WooCommerce.com * diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php new file mode 100644 index 00000000000..f38b49e1bb5 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php @@ -0,0 +1,101 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + $this->marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + } + + /** + * Test teardown. + */ + public function tearDown(): void { + $this->marketing_channels_service->unregister_all(); + parent::tearDown(); + } + + /** + * Tests that the marketing campaigns for all registered channels are aggregated and returned by the endpoint. + */ + public function test_returns_aggregated_marketing_campaigns() { + // Create a mock marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + // Create a mock marketing campaign type. + $test_campaign_type_1 = $this->createMock( MarketingCampaignType::class ); + $test_campaign_type_1->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-type-1' ); + $test_campaign_type_1->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_1 ); + // Return the sample campaign type by the mock marketing channel. + $test_channel_1->expects( $this->any() )->method( 'get_supported_campaign_types' )->willReturn( [ $test_campaign_type_1 ] ); + // Register the marketing channel. + $this->marketing_channels_service->register( $test_channel_1 ); + + // Create a second mock marketing channel. + $test_channel_2 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_2->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-2' ); + // Create a mock marketing campaign type for the second marketing channel. + $test_campaign_type_2 = $this->createMock( MarketingCampaignType::class ); + $test_campaign_type_2->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-type-2' ); + $test_campaign_type_2->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_2 ); + // Return the sample campaign by the second mock marketing channel. + $test_channel_2->expects( $this->any() )->method( 'get_supported_campaign_types' )->willReturn( [ $test_campaign_type_2 ] ); + // Register the second marketing channel. + $this->marketing_channels_service->register( $test_channel_2 ); + + $request = new WP_REST_Request( 'GET', self::ENDPOINT ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 2, $data ); + $this->assertEquals( + [ + 'test-campaign-type-1', + 'test-campaign-type-2', + ], + array_column( $data, 'id' ) + ); + } + +} diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php new file mode 100644 index 00000000000..341799b6127 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php @@ -0,0 +1,155 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + $this->marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + } + + /** + * Test teardown. + */ + public function tearDown(): void { + $this->marketing_channels_service->unregister_all(); + parent::tearDown(); + } + + /** + * Tests that the marketing campaigns for all registered channels are aggregated and returned by the endpoint. + */ + public function test_returns_aggregated_marketing_campaigns() { + // Create a mock marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + // Create a mock marketing campaign type. + $test_campaign_type_1 = $this->createMock( MarketingCampaignType::class ); + $test_campaign_type_1->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_1 ); + // Create a mock marketing campaign. + $test_campaign_1 = $this->createMock( MarketingCampaign::class ); + $test_campaign_1->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-1' ); + $test_campaign_1->expects( $this->any() )->method( 'get_type' )->willReturn( $test_campaign_type_1 ); + // Return the sample campaign by the mock marketing channel. + $test_channel_1->expects( $this->any() )->method( 'get_campaigns' )->willReturn( [ $test_campaign_1 ] ); + // Register the marketing channel. + $this->marketing_channels_service->register( $test_channel_1 ); + + // Create a second mock marketing channel. + $test_channel_2 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_2->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-2' ); + // Create a mock marketing campaign type for the second marketing channel. + $test_campaign_type_2 = $this->createMock( MarketingCampaignType::class ); + $test_campaign_type_2->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_2 ); + // Create a mock marketing campaign for the second marketing channel. + $test_campaign_2 = $this->createMock( MarketingCampaign::class ); + $test_campaign_2->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-2' ); + $test_campaign_2->expects( $this->any() )->method( 'get_type' )->willReturn( $test_campaign_type_2 ); + // Return the sample campaign by the second mock marketing channel. + $test_channel_2->expects( $this->any() )->method( 'get_campaigns' )->willReturn( [ $test_campaign_2 ] ); + // Register the second marketing channel. + $this->marketing_channels_service->register( $test_channel_2 ); + + $request = new WP_REST_Request( 'GET', self::ENDPOINT ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 2, $data ); + $this->assertEquals( + [ + 'test-campaign-1', + 'test-campaign-2', + ], + array_column( $data, 'id' ) + ); + $this->assertEquals( + [ + 'test-channel-1', + 'test-channel-2', + ], + array_column( $data, 'channel' ) + ); + } + + /** + * Tests that the marketing campaigns are paginated and then returned by the endpoint. + */ + public function test_paginates_marketing_campaigns() { + // Create a mock marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + // Return mock campaigns by the mock marketing channel. + $test_channel_1->expects( $this->any() )->method( 'get_campaigns' )->willReturn( + [ + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + ] + ); + // Register the marketing channel. + $this->marketing_channels_service->register( $test_channel_1 ); + + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( + [ + 'page' => '1', + 'per_page' => '2', + ] + ); + $response = $this->server->dispatch( $request ); + $headers = $response->get_headers(); + + $this->assertCount( 2, $response->get_data() ); + + $this->assertArrayHasKey( 'Link', $headers ); + $this->assertArrayHasKey( 'X-WP-Total', $headers ); + $this->assertArrayHasKey( 'X-WP-TotalPages', $headers ); + $this->assertEquals( 5, $headers['X-WP-Total'] ); + $this->assertEquals( 3, $headers['X-WP-TotalPages'] ); + } + +} diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php new file mode 100644 index 00000000000..9159b977ba4 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php @@ -0,0 +1,77 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + $this->marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + } + + /** + * Test teardown. + */ + public function tearDown(): void { + $this->marketing_channels_service->unregister_all(); + parent::tearDown(); + } + + /** + * Tests that the registered marketing channels are returned by the endpoint. + */ + public function test_returns_registered_marketing_channels() { + // Register marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + $test_channel_1->expects( $this->any() )->method( 'get_name' )->willReturn( 'Test Channel One' ); + $this->marketing_channels_service->register( $test_channel_1 ); + + $request = new WP_REST_Request( 'GET', self::ENDPOINT ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 1, $data ); + $this->assertEquals( 'test-channel-1', $data[0]['slug'] ); + $this->assertEquals( 'Test Channel One', $data[0]['name'] ); + } + +} diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php new file mode 100644 index 00000000000..46335da52e3 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php @@ -0,0 +1,138 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + set_transient( + MarketingSpecs::RECOMMENDED_PLUGINS_TRANSIENT, + [ + [ + 'title' => 'Example Marketing Channel', + 'description' => 'List your products and create ads, etc.', + 'url' => 'https://woocommerce.com/products/example-channel', + 'direct_install' => true, + 'icon' => 'https://woocommerce.com/example.svg', + 'product' => 'example-channel', + 'plugin' => 'example-channel/example-channel.php', + 'categories' => [ MarketingSpecs::MARKETING_EXTENSION_CATEGORY_SLUG ], + 'subcategories' => [ + [ + 'slug' => MarketingSpecs::MARKETING_CHANNEL_SUBCATEGORY_SLUG, + 'name' => 'Sales channels', + ], + ], + 'tags' => [], + ], + [ + 'title' => 'Example Marketing Extension', + 'description' => 'Automate your customer communications, etc.', + 'url' => 'https://woocommerce.com/products/example-marketing-extension', + 'direct_install' => true, + 'icon' => 'https://woocommerce.com/example-marketing-extension.svg', + 'product' => 'example-marketing-extension', + 'plugin' => 'example-marketing-extension/example-marketing-extension.php', + 'categories' => [ MarketingSpecs::MARKETING_EXTENSION_CATEGORY_SLUG ], + 'subcategories' => [ + [ + 'slug' => 'email', + 'name' => 'Email', + ], + ], + 'tags' => [], + ], + [ + 'title' => 'Example NON Marketing Extension', + 'description' => 'Handle coupons, etc.', + 'url' => 'https://woocommerce.com/products/example-random-extension', + 'direct_install' => true, + 'icon' => 'https://woocommerce.com/example-random-extension.svg', + 'product' => 'example-random-extension', + 'plugin' => 'example-random-extension/example-random-extension.php', + 'categories' => [ 'coupons' ], + 'subcategories' => [], + 'tags' => [], + ], + ] + ); + } + + /** + * Tests that the marketing channel recommendations are returned by the endpoint. + */ + public function test_returns_recommended_marketing_channels() { + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( [ 'category' => 'channels' ] ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 1, $data ); + $this->assertEquals( 'Example Marketing Channel', $data[0]['title'] ); + $this->assertEquals( 'example-channel', $data[0]['product'] ); + } + + /** + * Tests that the marketing extension recommendations are returned by the endpoint. + */ + public function test_returns_recommended_marketing_extensions() { + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( [ 'category' => 'extensions' ] ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 1, $data ); + $this->assertEquals( 'Example Marketing Extension', $data[0]['title'] ); + $this->assertEquals( 'example-marketing-extension', $data[0]['product'] ); + } + + /** + * Tests that the endpoint returns an error if the provided category is invalid. + */ + public function test_returns_error_if_invalid_category_provided() { + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( [ 'category' => 'test-non-existing-invalid-category' ] ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + $this->assertEquals( 'rest_invalid_param', $data['code'] ); + } + +} diff --git a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php index 401e1630294..4277cff9437 100644 --- a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php +++ b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php @@ -3,6 +3,7 @@ namespace Automattic\WooCommerce\Tests\Admin\Marketing; use Automattic\WooCommerce\Admin\Marketing\MarketingCampaign; +use Automattic\WooCommerce\Admin\Marketing\MarketingCampaignType; use Automattic\WooCommerce\Admin\Marketing\Price; use WC_Unit_Test_Case; @@ -12,12 +13,15 @@ use WC_Unit_Test_Case; class MarketingCampaignTest extends WC_Unit_Test_Case { /** - * @testdox `get_id`, `get_title`, `get_manage_url`, and `get_cost` return the class properties set by the constructor. + * @testdox `get_id`, `get_type`, `get_title`, `get_manage_url`, and `get_cost` return the class properties set by the constructor. */ public function test_get_methods_return_properties() { - $marketing_campaign = new MarketingCampaign( '1234', 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); + $test_campaign_type_1 = $this->createMock( MarketingCampaignType::class ); + + $marketing_campaign = new MarketingCampaign( '1234', $test_campaign_type_1, 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); $this->assertEquals( '1234', $marketing_campaign->get_id() ); + $this->assertEquals( $test_campaign_type_1, $marketing_campaign->get_type() ); $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() ); @@ -29,30 +33,10 @@ class MarketingCampaignTest extends WC_Unit_Test_Case { * @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' ); + $test_campaign_type_1 = $this->createMock( MarketingCampaignType::class ); + + $marketing_campaign = new MarketingCampaign( '1234', $test_campaign_type_1, '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 ) - ); - } } From 4059fbb33ba08f949bcc12835f00b45479a2cac6 Mon Sep 17 00:00:00 2001 From: Saad Tarhi Date: Mon, 16 Jan 2023 09:57:06 +0100 Subject: [PATCH 074/343] Fix PHPCS lint error: short array syntax --- .../woocommerce/src/Internal/Features/FeaturesController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php index 44f84e1a77a..47694ded9a3 100644 --- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php +++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php @@ -540,7 +540,7 @@ class FeaturesController { return $features[ $feature_id ]['is_experimental']; } ); - $mature_feature_ids = array_diff( $feature_ids, $experimental_feature_ids, [ 'cart_checkout_blocks' ] ); + $mature_feature_ids = array_diff( $feature_ids, $experimental_feature_ids, array( 'cart_checkout_blocks' ) ); $feature_ids = array_merge( $mature_feature_ids, array( 'mature_features_end' ), $experimental_feature_ids ); foreach ( $feature_ids as $id ) { From 7e27c0d67ac1aab6b2d19969a2f20e1acca24df8 Mon Sep 17 00:00:00 2001 From: Nima Date: Mon, 16 Jan 2023 17:12:16 +0000 Subject: [PATCH 075/343] Add changelog --- .../changelog/feature-34548-multichannel-marketing-backend | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/feature-34548-multichannel-marketing-backend diff --git a/plugins/woocommerce/changelog/feature-34548-multichannel-marketing-backend b/plugins/woocommerce/changelog/feature-34548-multichannel-marketing-backend new file mode 100644 index 00000000000..c23bfc56f5a --- /dev/null +++ b/plugins/woocommerce/changelog/feature-34548-multichannel-marketing-backend @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add multichannel marketing API From f616d688d87a21bd36b64b5edcbac89f9d4f110c Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Wed, 18 Jan 2023 02:04:42 +0800 Subject: [PATCH 076/343] Call marketing channels API with wp.data. --- .../data-multichannel/action-types.ts | 4 + .../marketing/data-multichannel/actions.ts | 24 ++++++ .../marketing/data-multichannel/constants.ts | 2 + .../marketing/data-multichannel/guards.ts | 14 ++++ .../marketing/data-multichannel/index.ts | 26 +++++++ .../marketing/data-multichannel/reducer.ts | 43 +++++++++++ .../marketing/data-multichannel/resolvers.ts | 28 +++++++ .../marketing/data-multichannel/selectors.ts | 8 ++ .../marketing/data-multichannel/types.ts | 27 +++++++ .../marketing/hooks/useRegisteredChannels.ts | 77 ++++++++++++++----- .../MarketingOverviewMultichannel.tsx | 1 + 11 files changed, 234 insertions(+), 20 deletions(-) create mode 100644 plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts create mode 100644 plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts create mode 100644 plugins/woocommerce-admin/client/marketing/data-multichannel/constants.ts create mode 100644 plugins/woocommerce-admin/client/marketing/data-multichannel/guards.ts create mode 100644 plugins/woocommerce-admin/client/marketing/data-multichannel/index.ts create mode 100644 plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts create mode 100644 plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts create mode 100644 plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts create mode 100644 plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts new file mode 100644 index 00000000000..06c9e42a1db --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts @@ -0,0 +1,4 @@ +export const TYPES = { + RECEIVE_CHANNELS_SUCCESS: 'RECEIVE_CHANNELS_SUCCESS' as const, + RECEIVE_CHANNELS_ERROR: 'RECEIVE_CHANNELS_ERROR' as const, +}; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts new file mode 100644 index 00000000000..2e0986b0f3d --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts @@ -0,0 +1,24 @@ +/** + * Internal dependencies + */ +import { TYPES } from './action-types'; +import { ApiFetchError, Channel } from './types'; + +export const receiveChannelsSuccess = ( channels: Array< Channel > ) => { + return { + type: TYPES.RECEIVE_CHANNELS_SUCCESS, + payload: channels, + }; +}; + +export const receiveChannelsError = ( error: ApiFetchError ) => { + return { + type: TYPES.RECEIVE_CHANNELS_ERROR, + payload: error, + error: true, + }; +}; + +export type Action = ReturnType< + typeof receiveChannelsSuccess | typeof receiveChannelsError +>; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/constants.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/constants.ts new file mode 100644 index 00000000000..e57d9c8406e --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/constants.ts @@ -0,0 +1,2 @@ +export const STORE_KEY = 'wc/marketing-multichannel'; +export const API_NAMESPACE = '/wc-admin/marketing'; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/guards.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/guards.ts new file mode 100644 index 00000000000..b8906cf2ed9 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/guards.ts @@ -0,0 +1,14 @@ +/** + * Internal dependencies + */ +import { ApiFetchError } from './types'; + +export const isObject = ( obj: unknown ): obj is Record< string, unknown > => { + return !! obj && typeof obj === 'object'; +}; + +export const isApiFetchError = ( obj: unknown ): obj is ApiFetchError => { + return ( + isObject( obj ) && 'code' in obj && 'data' in obj && 'message' in obj + ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/index.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/index.ts new file mode 100644 index 00000000000..1c213637e2f --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/index.ts @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { createReduxStore, register } from '@wordpress/data'; +import { Reducer, AnyAction } from 'redux'; +import { controls } from '@wordpress/data-controls'; + +/** + * Internal dependencies + */ +import { State } from './types'; +import { STORE_KEY } from './constants'; +import { reducer } from './reducer'; +import * as actions from './actions'; +import * as selectors from './selectors'; +import * as resolvers from './resolvers'; + +const store = createReduxStore( STORE_KEY, { + reducer: reducer as Reducer< State, AnyAction >, + actions, + selectors, + resolvers, + controls, +} ); + +register( store ); diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts new file mode 100644 index 00000000000..b6a643402a8 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts @@ -0,0 +1,43 @@ +/** + * External dependencies + */ + +import type { Reducer } from 'redux'; + +/** + * Internal dependencies + */ +import { State } from './types'; +import { Action } from './actions'; +import { TYPES } from './action-types'; + +const initialState = { + channels: { + data: undefined, + error: undefined, + }, +}; + +export const reducer: Reducer< State, Action > = ( + state = initialState, + action +) => { + switch ( action.type ) { + case TYPES.RECEIVE_CHANNELS_SUCCESS: + return { + ...state, + channels: { + data: action.payload, + }, + }; + case TYPES.RECEIVE_CHANNELS_ERROR: + return { + ...state, + channels: { + error: action.payload, + }, + }; + default: + return state; + } +}; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts new file mode 100644 index 00000000000..cf55b4b1790 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts @@ -0,0 +1,28 @@ +/** + * External dependencies + */ +import { apiFetch } from '@wordpress/data-controls'; + +/** + * Internal dependencies + */ +import { receiveChannelsSuccess, receiveChannelsError } from './actions'; +import { Channel } from './types'; +import { API_NAMESPACE } from './constants'; +import { isApiFetchError } from './guards'; + +export function* getChannels() { + try { + const data: Channel[] = yield apiFetch( { + path: `${ API_NAMESPACE }/channels`, + } ); + + yield receiveChannelsSuccess( data ); + } catch ( error ) { + if ( isApiFetchError( error ) ) { + yield receiveChannelsError( error ); + } + + throw error; + } +} diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts new file mode 100644 index 00000000000..6626e622a1b --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts @@ -0,0 +1,8 @@ +/** + * Internal dependencies + */ +import { State } from './types'; + +export const getChannels = ( state: State ) => { + return state.channels; +}; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts new file mode 100644 index 00000000000..1e213b31f54 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts @@ -0,0 +1,27 @@ +export type ApiFetchError = { + code: string; + data: { + status: number; + }; + message: string; +}; + +export type Channel = { + slug: string; + is_setup_completed: boolean; + settings_url: string; + name: string; + description: string; + product_listings_status: string; + errors_count: number; + icon: string; +}; + +export type Channels = { + data?: Array< Channel >; + error?: ApiFetchError; +}; + +export type State = { + channels: Channels; +}; diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts index d512f1d4d32..73b77ebb187 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts @@ -1,11 +1,19 @@ +/** + * External dependencies + */ +import { useSelect } from '@wordpress/data'; + /** * Internal dependencies */ import { InstalledChannel } from '~/marketing/types'; +import { STORE_KEY } from '~/marketing/data-multichannel/constants'; +import { ApiFetchError, Channel, Channels } from '../data-multichannel/types'; type UseRegisteredChannels = { loading: boolean; data: Array< InstalledChannel >; + error?: ApiFetchError; }; // // TODO: To be removed. This is for testing loading state. @@ -66,26 +74,55 @@ type UseRegisteredChannels = { // }; // }; -// TODO: To be removed. This is for testing everything works okay. -export const useRegisteredChannels = (): UseRegisteredChannels => { - // TODO: call API here to get data. - // The following are just dummy data for testing now. +// // TODO: To be removed. This is for testing everything works okay. +// export const useRegisteredChannels = (): UseRegisteredChannels => { +// // TODO: call API here to get data. +// // The following are just dummy data for testing now. +// return { +// loading: false, +// data: [ +// { +// slug: 'google-listings-and-ads', +// title: 'Google Listings and Ads', +// description: +// 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', +// icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', +// isSetupCompleted: true, +// setupUrl: 'https://www.example.com/setup', +// manageUrl: 'https://www.example.com/manage', +// syncStatus: 'synced' as const, +// issueType: 'none' as const, +// issueText: 'No issues to resolve', +// }, +// ], +// }; +// }; + +const convert = ( data: Channel ): InstalledChannel => { + // TODO: map all the fields correctly from API to UI. return { - loading: false, - data: [ - { - slug: 'google-listings-and-ads', - title: 'Google Listings and Ads', - description: - 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', - icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', - isSetupCompleted: true, - setupUrl: 'https://www.example.com/setup', - manageUrl: 'https://www.example.com/manage', - syncStatus: 'synced' as const, - issueType: 'none' as const, - issueText: 'No issues to resolve', - }, - ], + slug: data.slug, + title: data.name, + description: data.description, + icon: data.icon, + isSetupCompleted: data.is_setup_completed, + setupUrl: data.settings_url, + manageUrl: data.settings_url, + syncStatus: 'synced', + issueType: 'none', + issueText: '', }; }; + +export const useRegisteredChannels = (): UseRegisteredChannels => { + return useSelect( ( select ) => { + const { hasFinishedResolution, getChannels } = select( STORE_KEY ); + const channels = getChannels< Channels >(); + + return { + loading: ! hasFinishedResolution( 'getChannels' ), + data: channels.data?.map( convert ) || [], + error: channels.error, + }; + } ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx index 7cbad05d6a2..a34e1a84f67 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx @@ -12,6 +12,7 @@ import { InstalledExtensions } from './InstalledExtensions'; import { DiscoverTools } from './DiscoverTools'; import { LearnMarketing } from './LearnMarketing'; import '~/marketing/data'; +import '~/marketing/data-multichannel'; import { useRegisteredChannels, useRecommendedChannels, From 0a36d7a4fe621ecc1904d70070fa8a0c9ab465c1 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Thu, 19 Jan 2023 01:12:50 +0800 Subject: [PATCH 077/343] Map errors_count to issueType and issueText. --- .../marketing/hooks/useRegisteredChannels.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts index 73b77ebb187..8399a7a04f1 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts @@ -2,6 +2,7 @@ * External dependencies */ import { useSelect } from '@wordpress/data'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies @@ -100,6 +101,17 @@ type UseRegisteredChannels = { const convert = ( data: Channel ): InstalledChannel => { // TODO: map all the fields correctly from API to UI. + + const issueType = data.errors_count >= 1 ? 'error' : 'none'; + const issueText = + data.errors_count >= 1 + ? sprintf( + // translators: %d: The number of issues to resolve. + __( '%d issues to resolve', 'woocommerce' ), + data.errors_count + ) + : __( 'No issues to resolve', 'woocommerce' ); + return { slug: data.slug, title: data.name, @@ -109,8 +121,8 @@ const convert = ( data: Channel ): InstalledChannel => { setupUrl: data.settings_url, manageUrl: data.settings_url, syncStatus: 'synced', - issueType: 'none', - issueText: '', + issueType, + issueText, }; }; From 2d7c8db2e5b49c2d268e3a2d43605c777ec89d35 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Thu, 19 Jan 2023 01:32:38 +0800 Subject: [PATCH 078/343] Map product_listings_status to syncStatus. --- .../marketing/hooks/useRegisteredChannels.ts | 17 ++++++++++++----- .../Channels/InstalledChannelCardBody.tsx | 8 ++++++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts index 8399a7a04f1..d23fc705ca8 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts @@ -7,9 +7,13 @@ import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ -import { InstalledChannel } from '~/marketing/types'; +import { InstalledChannel, SyncStatusType } from '~/marketing/types'; import { STORE_KEY } from '~/marketing/data-multichannel/constants'; -import { ApiFetchError, Channel, Channels } from '../data-multichannel/types'; +import { + ApiFetchError, + Channel, + Channels, +} from '~/marketing/data-multichannel/types'; type UseRegisteredChannels = { loading: boolean; @@ -99,9 +103,12 @@ type UseRegisteredChannels = { // }; // }; -const convert = ( data: Channel ): InstalledChannel => { - // TODO: map all the fields correctly from API to UI. +const statusMap: Record< string, SyncStatusType > = { + synced: 'synced', + 'sync-in-progress': 'syncing', +}; +const convert = ( data: Channel ): InstalledChannel => { const issueType = data.errors_count >= 1 ? 'error' : 'none'; const issueText = data.errors_count >= 1 @@ -120,7 +127,7 @@ const convert = ( data: Channel ): InstalledChannel => { isSetupCompleted: data.is_setup_completed, setupUrl: data.settings_url, manageUrl: data.settings_url, - syncStatus: 'synced', + syncStatus: statusMap[ data.product_listings_status ], issueType, issueText, }; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx index abd7f85dc5f..9be8d93c417 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx @@ -115,8 +115,12 @@ export const InstalledChannelCardBody: React.FC< installedChannel.description ) : (
- -
+ { installedChannel.syncStatus && ( + <> + +
+ + ) }
); From 49744446a1946f5d448e89da869fd5cca810f91a Mon Sep 17 00:00:00 2001 From: Lucas Bustamante Date: Wed, 18 Jan 2023 18:56:15 -0300 Subject: [PATCH 079/343] Make the selector a bit more strict to avoid collision --- .../tests/e2e-pw/tests/merchant/order-refund.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-refund.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-refund.spec.js index 1b84805f475..84685ba7300 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-refund.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-refund.spec.js @@ -233,7 +233,7 @@ test.describe( 'WooCommerce Orders > Refund and restock an order item', () => { // Update the order await page.click( 'button.save_order' ); - await expect( page.locator( 'div.notice-success' ) ).toContainText( + await expect( page.locator( 'div.updated.notice-success' ) ).toContainText( 'Order updated.' ); From b708cc18ff0a7cdc046853822ab240b500fc7649 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Thu, 19 Jan 2023 19:33:51 +0800 Subject: [PATCH 080/343] Get recommended channels from API. --- .../data-multichannel/action-types.ts | 4 + .../marketing/data-multichannel/actions.ts | 24 ++- .../marketing/data-multichannel/reducer.ts | 18 +++ .../marketing/data-multichannel/resolvers.ts | 25 ++- .../marketing/data-multichannel/selectors.ts | 4 + .../marketing/data-multichannel/types.ts | 32 ++++ .../marketing/hooks/useRecommendedChannels.ts | 153 +++--------------- 7 files changed, 124 insertions(+), 136 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts index 06c9e42a1db..c53be7bbd49 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts @@ -1,4 +1,8 @@ export const TYPES = { RECEIVE_CHANNELS_SUCCESS: 'RECEIVE_CHANNELS_SUCCESS' as const, RECEIVE_CHANNELS_ERROR: 'RECEIVE_CHANNELS_ERROR' as const, + RECEIVE_RECOMMENDED_CHANNELS_SUCCESS: + 'RECEIVE_RECOMMENDED_CHANNELS_SUCCESS' as const, + RECEIVE_RECOMMENDED_CHANNELS_ERROR: + 'RECEIVE_RECOMMENDED_CHANNELS_ERROR' as const, }; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts index 2e0986b0f3d..74785dbc5ce 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts @@ -2,7 +2,7 @@ * Internal dependencies */ import { TYPES } from './action-types'; -import { ApiFetchError, Channel } from './types'; +import { ApiFetchError, Channel, RecommendedPlugin } from './types'; export const receiveChannelsSuccess = ( channels: Array< Channel > ) => { return { @@ -19,6 +19,26 @@ export const receiveChannelsError = ( error: ApiFetchError ) => { }; }; +export const receiveRecommendedChannelsSuccess = ( + channels: Array< RecommendedPlugin > +) => { + return { + type: TYPES.RECEIVE_RECOMMENDED_CHANNELS_SUCCESS, + payload: channels, + }; +}; + +export const receiveRecommendedChannelsError = ( error: ApiFetchError ) => { + return { + type: TYPES.RECEIVE_RECOMMENDED_CHANNELS_ERROR, + payload: error, + error: true, + }; +}; + export type Action = ReturnType< - typeof receiveChannelsSuccess | typeof receiveChannelsError + | typeof receiveChannelsSuccess + | typeof receiveChannelsError + | typeof receiveRecommendedChannelsSuccess + | typeof receiveRecommendedChannelsError >; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts index b6a643402a8..d9e95181bf6 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts @@ -16,6 +16,10 @@ const initialState = { data: undefined, error: undefined, }, + recommendedChannels: { + data: undefined, + error: undefined, + }, }; export const reducer: Reducer< State, Action > = ( @@ -37,6 +41,20 @@ export const reducer: Reducer< State, Action > = ( error: action.payload, }, }; + case TYPES.RECEIVE_RECOMMENDED_CHANNELS_SUCCESS: + return { + ...state, + recommendedChannels: { + data: action.payload, + }, + }; + case TYPES.RECEIVE_RECOMMENDED_CHANNELS_ERROR: + return { + ...state, + recommendedChannels: { + error: action.payload, + }, + }; default: return state; } diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts index cf55b4b1790..deb68f3b9cf 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts @@ -6,8 +6,13 @@ import { apiFetch } from '@wordpress/data-controls'; /** * Internal dependencies */ -import { receiveChannelsSuccess, receiveChannelsError } from './actions'; -import { Channel } from './types'; +import { + receiveChannelsSuccess, + receiveChannelsError, + receiveRecommendedChannelsSuccess, + receiveRecommendedChannelsError, +} from './actions'; +import { Channel, RecommendedPlugin } from './types'; import { API_NAMESPACE } from './constants'; import { isApiFetchError } from './guards'; @@ -26,3 +31,19 @@ export function* getChannels() { throw error; } } + +export function* getRecommendedChannels() { + try { + const data: RecommendedPlugin[] = yield apiFetch( { + path: `${ API_NAMESPACE }/recommendations?category=channels`, + } ); + + yield receiveRecommendedChannelsSuccess( data ); + } catch ( error ) { + if ( isApiFetchError( error ) ) { + yield receiveRecommendedChannelsError( error ); + } + + throw error; + } +} diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts index 6626e622a1b..c37bb8599ad 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts @@ -6,3 +6,7 @@ import { State } from './types'; export const getChannels = ( state: State ) => { return state.channels; }; + +export const getRecommendedChannels = ( state: State ) => { + return state.recommendedChannels; +}; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts index 1e213b31f54..9f412d0b445 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts @@ -22,6 +22,38 @@ export type Channels = { error?: ApiFetchError; }; +// TODO: The following types are copied from plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/types.ts. +// They are may be changed later, depending on the outcome of API development. + +type Subcategory = { + slug: string; + name: string; +}; + +type Tag = { + slug: string; + name: string; +}; + +export type RecommendedPlugin = { + title: string; + description: string; + url: string; + direct_install: boolean; + icon: string; + product: string; + plugin: string; + categories: Array< string >; + subcategories: Array< Subcategory >; + tags: Array< Tag >; +}; + +export type RecommendedChannels = { + data?: Array< RecommendedPlugin >; + error?: ApiFetchError; +}; + export type State = { channels: Channels; + recommendedChannels: RecommendedChannels; }; diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts index 88982ec5bed..f8c03b163da 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts @@ -1,144 +1,33 @@ +/** + * External dependencies + */ +import { useSelect } from '@wordpress/data'; + /** * Internal dependencies */ +import { STORE_KEY } from '~/marketing/data-multichannel/constants'; import { RecommendedChannel } from '~/marketing/types'; +import { RecommendedChannels } from '~/marketing/data-multichannel/types'; type UseRecommendedChannels = { loading: boolean; data: Array< RecommendedChannel >; }; -// // TODO: to be removed. This is to test for loading state. -// export const useRecommendedChannels = (): UseRecommendedChannels => { -// // TODO: call API here to get data. -// // The following are just dummy data for testing now. -// return { -// loading: true, -// data: [], -// }; -// }; - -// // TODO: to be removed. This is to test for empty data. -// export const useRecommendedChannels = (): UseRecommendedChannels => { -// TODO: call API here to get data. -// The following are just dummy data for testing now. -// return { -// loading: false, -// data: [], -// }; -// }; - export const useRecommendedChannels = (): UseRecommendedChannels => { - // TODO: call API here to get data. - // The following are just dummy data for testing now. - return { - loading: false, - data: [ - { - title: 'Facebook for WooCommerce', - description: - 'List your products and create ads on Facebook and Instagram.', - url: 'https://woocommerce.com/products/facebook/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', - direct_install: true, - icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/facebook.svg', - product: 'facebook-for-woocommerce', - plugin: 'facebook-for-woocommerce/facebook-for-woocommerce.php', - categories: [ 'marketing' ], - subcategories: [ - { - slug: 'sales-channels', - name: 'Sales channels', - }, - ], - tags: [ - { - slug: 'built-by-woocommerce', - name: 'Built by WooCommerce', - }, - ], - }, - { - title: 'Google Listings and Ads', - description: - 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', - url: 'https://woocommerce.com/products/google-listings-and-ads/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', - direct_install: true, - icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', - product: 'google-listings-and-ads', - plugin: 'google-listings-and-ads/google-listings-and-ads.php', - categories: [ 'marketing' ], - subcategories: [ - { - slug: 'sales-channels', - name: 'Sales channels', - }, - ], - tags: [ - { - slug: 'built-by-woocommerce', - name: 'Built by WooCommerce', - }, - ], - }, - { - title: 'Pinterest for WooCommerce', - description: - 'Grow your business on Pinterest! Use this official plugin to allow shoppers to Pin products while browsing your store, track conversions, and advertise on Pinterest.', - url: 'https://woocommerce.com/products/pinterest-for-woocommerce/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', - direct_install: true, - icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/pinterest.svg', - product: 'pinterest-for-woocommerce', - plugin: 'pinterest-for-woocommerce/pinterest-for-woocommerce.php', - categories: [ 'marketing' ], - subcategories: [ - { - slug: 'sales-channels', - name: 'Sales channels', - }, - ], - tags: [ - { - slug: 'built-by-woocommerce', - name: 'Built by WooCommerce', - }, - ], - }, - { - title: 'TikTok for WooCommerce', - description: - 'Create advertising campaigns and reach one billion global users with TikTok for WooCommerce.', - url: 'https://woocommerce.com/products/tiktok-for-woocommerce/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', - direct_install: true, - icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/tiktok.jpg', - product: 'tiktok-for-business', - plugin: 'tiktok-for-business/tiktok-for-woocommerce.php', - categories: [ 'marketing' ], - subcategories: [ - { - slug: 'sales-channels', - name: 'Sales channels', - }, - ], - tags: [], - }, - { - title: 'Amazon, eBay & Walmart Integration for WooCommerce', - description: - 'Get the official Amazon, eBay and Walmart extension and create, sync and manage multichannel listings directly from WooCommerce.', - url: 'https://woocommerce.com/products/amazon-ebay-integration/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', - direct_install: false, - icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/amazon-ebay.svg', - product: 'amazon-ebay-integration', - plugin: 'woocommerce-amazon-ebay-integration/woocommerce-amazon-ebay-integration.php', - categories: [ 'marketing' ], - subcategories: [ - { - slug: 'sales-channels', - name: 'Sales channels', - }, - ], - tags: [], - }, - ], - }; + return useSelect( ( select ) => { + const { hasFinishedResolution, getRecommendedChannels } = + select( STORE_KEY ); + const channels = getRecommendedChannels< RecommendedChannels >(); + + // TODO: filter recommended channels against installed plugins, + // using @woocommerce/data/plugins. + + return { + loading: ! hasFinishedResolution( 'getChannels' ), + data: channels.data || [], + error: channels.error, + }; + } ); }; From cf4c62e7076f91f84fd0e43e45fba9ce1d9b1bc8 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Thu, 19 Jan 2023 19:50:56 +0800 Subject: [PATCH 081/343] Fix loading bug in useRecommendedChannels. --- .../client/marketing/hooks/useRecommendedChannels.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts index f8c03b163da..1058ae364a0 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts @@ -25,7 +25,7 @@ export const useRecommendedChannels = (): UseRecommendedChannels => { // using @woocommerce/data/plugins. return { - loading: ! hasFinishedResolution( 'getChannels' ), + loading: ! hasFinishedResolution( 'getRecommendedChannels' ), data: channels.data || [], error: channels.error, }; From 223ea4d7127d0592d1e754339725f0109fe4688b Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Thu, 19 Jan 2023 22:23:00 +0800 Subject: [PATCH 082/343] Filter recommended channels to get "not installed" or "not activated" channels. --- .../marketing/hooks/useRecommendedChannels.ts | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts index 1058ae364a0..7111f776d59 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts @@ -2,6 +2,8 @@ * External dependencies */ import { useSelect } from '@wordpress/data'; +import { PLUGINS_STORE_NAME } from '@woocommerce/data'; +import { differenceWith } from 'lodash'; /** * Internal dependencies @@ -19,15 +21,27 @@ export const useRecommendedChannels = (): UseRecommendedChannels => { return useSelect( ( select ) => { const { hasFinishedResolution, getRecommendedChannels } = select( STORE_KEY ); - const channels = getRecommendedChannels< RecommendedChannels >(); + const { data, error } = getRecommendedChannels< RecommendedChannels >(); - // TODO: filter recommended channels against installed plugins, - // using @woocommerce/data/plugins. + const { getActivePlugins } = select( PLUGINS_STORE_NAME ); + const activePlugins = getActivePlugins(); + + /** + * Recommended channels that are not in "active" state, + * i.e. channels that are not installed or not activated yet. + */ + const nonActiveRecommendedChannels = differenceWith( + data, + activePlugins, + ( a, b ) => { + return a.product === b; + } + ); return { loading: ! hasFinishedResolution( 'getRecommendedChannels' ), - data: channels.data || [], - error: channels.error, + data: nonActiveRecommendedChannels, + error, }; } ); }; From 9fa6ea4a25ae393a033d7d469ce84f365eafb0ed Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 20 Jan 2023 00:36:34 +0800 Subject: [PATCH 083/343] Display activate button for channels that are not yet activated. --- .../PluginCardBody/SmartPluginCardBody.tsx | 15 +++++++++++ .../PluginCardBody/useIsPluginInstalled.ts | 25 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 plugins/woocommerce-admin/client/marketing/components/PluginCardBody/useIsPluginInstalled.ts diff --git a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx index f3272e48f78..b6375d82109 100644 --- a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx @@ -16,6 +16,7 @@ import { PluginCardBody } from '~/marketing/components'; import { RecommendedPlugin } from '~/marketing/types'; import { getInAppPurchaseUrl } from '~/lib/in-app-purchase'; import { createNoticesFromResponse } from '~/lib/notices'; +import { useIsPluginInstalled } from './useIsPluginInstalled'; import './PluginCardBody.scss'; type SmartPluginCardBodyProps = { @@ -38,6 +39,7 @@ export const SmartPluginCardBody = ( { null ); const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME ); + const { isPluginInstalled } = useIsPluginInstalled(); /** * Install and activate a plugin. @@ -71,6 +73,19 @@ export const SmartPluginCardBody = ( { const renderButton = () => { const buttonDisabled = !! currentPlugin; + if ( isPluginInstalled( plugin.product ) ) { + return ( + + ); + } + if ( plugin.direct_install ) { return (
diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx index d88bef12f29..9aeac78e5ec 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx @@ -14,17 +14,21 @@ import './Channels.scss'; type RecommendedChannelListPropsType = { recommendedChannels: Array< RecommendedChannel >; + onInstalledAndActivated?: () => void; }; export const RecommendedChannelsList: React.FC< RecommendedChannelListPropsType -> = ( { recommendedChannels } ) => { +> = ( { recommendedChannels, onInstalledAndActivated } ) => { return ( <> { recommendedChannels.map( ( el, idx ) => { return ( - + { idx < recommendedChannels.length - 1 && ( ) } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx index a34e1a84f67..838a45ecbf0 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx @@ -21,8 +21,11 @@ import './MarketingOverviewMultichannel.scss'; import { CenteredSpinner } from '../components'; export const MarketingOverviewMultichannel: React.FC = () => { - const { loading: loadingRegistered, data: dataRegistered } = - useRegisteredChannels(); + const { + loading: loadingRegistered, + data: dataRegistered, + refetch, + } = useRegisteredChannels(); const { loading: loadingRecommended, data: dataRecommended } = useRecommendedChannels(); const { currentUserCan } = useUser(); @@ -31,18 +34,25 @@ export const MarketingOverviewMultichannel: React.FC = () => { getAdminSetting( 'allowMarketplaceSuggestions', false ) && currentUserCan( 'install_plugins' ); - if ( loadingRegistered || loadingRecommended ) { + if ( + ( loadingRegistered && ! dataRegistered ) || + ( loadingRecommended && ! dataRecommended ) + ) { return ; } return (
- { ( dataRegistered.length >= 1 || dataRecommended.length >= 1 ) && ( - - ) } + { dataRegistered && + dataRecommended && + ( dataRegistered.length >= 1 || + dataRecommended.length >= 1 ) && ( + + ) } { shouldShowExtensions && } From d69f078fd551aee5f44781169a619e9495dbe12f Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 20 Jan 2023 01:36:39 +0800 Subject: [PATCH 085/343] Replace useIsPluginInstalled with useIsPluginInstalledNotActivated. --- .../PluginCardBody/SmartPluginCardBody.tsx | 7 +++--- .../PluginCardBody/useIsPluginInstalled.ts | 25 ------------------- .../useIsPluginInstalledNotActivated.ts | 24 ++++++++++++++++++ 3 files changed, 28 insertions(+), 28 deletions(-) delete mode 100644 plugins/woocommerce-admin/client/marketing/components/PluginCardBody/useIsPluginInstalled.ts create mode 100644 plugins/woocommerce-admin/client/marketing/components/PluginCardBody/useIsPluginInstalledNotActivated.ts diff --git a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx index b6375d82109..32262e7139c 100644 --- a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx @@ -16,7 +16,7 @@ import { PluginCardBody } from '~/marketing/components'; import { RecommendedPlugin } from '~/marketing/types'; import { getInAppPurchaseUrl } from '~/lib/in-app-purchase'; import { createNoticesFromResponse } from '~/lib/notices'; -import { useIsPluginInstalled } from './useIsPluginInstalled'; +import { useIsPluginInstalledNotActivated } from './useIsPluginInstalledNotActivated'; import './PluginCardBody.scss'; type SmartPluginCardBodyProps = { @@ -39,7 +39,8 @@ export const SmartPluginCardBody = ( { null ); const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME ); - const { isPluginInstalled } = useIsPluginInstalled(); + const { isPluginInstalledNotActivated } = + useIsPluginInstalledNotActivated(); /** * Install and activate a plugin. @@ -73,7 +74,7 @@ export const SmartPluginCardBody = ( { const renderButton = () => { const buttonDisabled = !! currentPlugin; - if ( isPluginInstalled( plugin.product ) ) { + if ( isPluginInstalledNotActivated( plugin.product ) ) { return ( ) : ( - ); @@ -147,11 +147,11 @@ export const InstalledChannelCardBody: React.FC< className="woocommerce-marketing-installed-channel-card-body" icon={ { } - name={ installedChannel.title } + name={ registeredChannel.title } description={ description } button={ button } /> diff --git a/plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts b/plugins/woocommerce-admin/client/marketing/types/RegisteredChannel.ts similarity index 90% rename from plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts rename to plugins/woocommerce-admin/client/marketing/types/RegisteredChannel.ts index c4e735089ac..24ddd4c6f54 100644 --- a/plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts +++ b/plugins/woocommerce-admin/client/marketing/types/RegisteredChannel.ts @@ -1,7 +1,7 @@ export type SyncStatusType = 'synced' | 'syncing' | 'failed'; export type IssueTypeType = 'error' | 'warning' | 'none'; -export type InstalledChannel = { +export type RegisteredChannel = { slug: string; title: string; description: string; diff --git a/plugins/woocommerce-admin/client/marketing/types/index.ts b/plugins/woocommerce-admin/client/marketing/types/index.ts index 24fbe739fd0..38327962d5d 100644 --- a/plugins/woocommerce-admin/client/marketing/types/index.ts +++ b/plugins/woocommerce-admin/client/marketing/types/index.ts @@ -3,5 +3,5 @@ export { RecommendedPlugin } from './RecommendedPlugin'; export { SyncStatusType, IssueTypeType, - InstalledChannel, -} from './InstalledChannel'; + RegisteredChannel, +} from './RegisteredChannel'; From 1af1524b83870a50cc1066c8a4e8a82e90ea53c9 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 21 Jan 2023 02:38:00 +0800 Subject: [PATCH 091/343] Rename CSS installed-channel to registered-channel. --- .../Channels/RegisteredChannelCardBody.scss | 4 ++-- .../Channels/RegisteredChannelCardBody.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.scss index 3a80fa7ef64..db8b73f9a24 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.scss @@ -1,5 +1,5 @@ -.woocommerce-marketing-installed-channel-card-body { - .woocommerce-marketing-installed-channel-description { +.woocommerce-marketing-registered-channel-card-body { + .woocommerce-marketing-registered-channel-description { display: flex; gap: $gap-smaller; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx index 9bdbfb871a3..f25b539a893 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx @@ -114,11 +114,11 @@ export const RegisteredChannelCardBody: React.FC< const description = ! registeredChannel.isSetupCompleted ? ( registeredChannel.description ) : ( -
+
{ registeredChannel.syncStatus && ( <> -
+
) } @@ -144,7 +144,7 @@ export const RegisteredChannelCardBody: React.FC< return ( Date: Mon, 10 Oct 2022 13:45:18 +0530 Subject: [PATCH 092/343] Revert "Revert "[COT] Add the orders cache" (#34992)" This reverts commit 0b5d7ab9ad4ef09fc0ef47c5deddfe2aa4f7e5c3. --- plugins/woocommerce/changelog/add-order_cache | 4 + .../includes/abstracts/abstract-wc-order.php | 7 + .../admin/class-wc-admin-settings.php | 3 + .../includes/class-wc-order-factory.php | 45 +++++- plugins/woocommerce/src/Caches/OrderCache.php | 44 ++++++ .../src/Caches/OrderCacheController.php | 121 ++++++++++++++++ .../woocommerce/src/Caching/ObjectCache.php | 74 ++++++++-- .../Orders/CustomOrdersTableController.php | 44 +++++- .../DataStores/Orders/DataSynchronizer.php | 6 + .../COTMigrationServiceProvider.php | 3 +- .../OrdersDataStoreServiceProvider.php | 7 + .../Internal/Features/FeaturesController.php | 5 + .../woocommerce/src/Utilities/OrderUtil.php | 11 ++ .../class-wc-rest-orders-controller-tests.php | 7 +- .../tests/php/src/Caching/ObjectCacheTest.php | 131 ++++++++++++++---- 15 files changed, 460 insertions(+), 52 deletions(-) create mode 100644 plugins/woocommerce/changelog/add-order_cache create mode 100644 plugins/woocommerce/src/Caches/OrderCache.php create mode 100644 plugins/woocommerce/src/Caches/OrderCacheController.php diff --git a/plugins/woocommerce/changelog/add-order_cache b/plugins/woocommerce/changelog/add-order_cache new file mode 100644 index 00000000000..1b800aff214 --- /dev/null +++ b/plugins/woocommerce/changelog/add-order_cache @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add a cache for orders, to use when custom order tables are enabled diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php index 78932ad68bc..bebe53ada46 100644 --- a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php @@ -10,9 +10,11 @@ * @package WooCommerce\Classes */ +use Automattic\WooCommerce\Caches\OrderCache; use Automattic\WooCommerce\Proxies\LegacyProxy; use Automattic\WooCommerce\Utilities\ArrayUtil; use Automattic\WooCommerce\Utilities\NumberUtil; +use Automattic\WooCommerce\Utilities\OrderUtil; defined( 'ABSPATH' ) || exit; @@ -203,6 +205,11 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { $this->save_items(); + if ( OrderUtil::orders_cache_usage_is_enabled() ) { + $order_cache = wc_get_container()->get( OrderCache::class ); + $order_cache->update_if_cached( $this ); + } + /** * Trigger action after saving to the DB. * diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-settings.php b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php index 477ae41ea61..2958d949806 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-settings.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php @@ -488,6 +488,8 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) : $visibility_class[] = 'show_options_if_checked'; } + $must_disable = ArrayUtil::get_value_or_default( $value, 'disabled', false ); + if ( ! isset( $value['checkboxgroup'] ) || 'start' === $value['checkboxgroup'] ) { $has_tooltip = isset( $value['tooltip'] ) && '' !== $value['tooltip']; $tooltip_container_class = $has_tooltip ? 'with-tooltip' : ''; @@ -515,6 +517,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) : ?>
{ + return [ + { + display: ( + + + { + + + + + + { el.title } + + + { el.description && ( + + { el.description } + + ) } + + + + ), + }, + { display: el.cost }, + ]; + } ) } + /> + { total > perPage && ( + + { + setPage( newPage ); + } } + /> + + ) } + + ); + }; return ( @@ -104,66 +158,7 @@ export const Campaigns = () => { { __( 'Campaigns', 'woocommerce' ) } -
{ - return [ - { - display: ( - - - { - - - - - - { el.title } - - - { el.description && ( - - { el.description } - - ) } - - - - ), - }, - { display: el.cost }, - ]; - } ) } - /> - { total > perPage && ( - - { - setPage( newPage ); - } } - /> - - ) } + { getContent() } ); }; From 7b068bf908cce666a9abc91983a9765130fe9842 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Thu, 2 Feb 2023 17:52:22 +0800 Subject: [PATCH 141/343] Code refactor for Campaigns card CSS. --- .../overview-multichannel/Campaigns/Campaigns.scss | 8 ++++---- .../overview-multichannel/Campaigns/Campaigns.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss index 190849bd5d7..720b6e2ae16 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss @@ -32,13 +32,13 @@ margin-bottom: $gap-smallest; } - .woocommerce-marketing-campaign-logo { + &__campaign-logo { img { display: block; } } - .woocommerce-marketing-campaign-title { + &__campaign-title { a { color: #007cba; font-weight: 600; @@ -46,11 +46,11 @@ } } - .woocommerce-marketing-campaign-description { + &__campaign-description { color: $gray-700; } - .woocommerce-marketing-campaigns-card-footer { + &__footer { justify-content: center; // Remove the border-top because the table cells already has a border-bottom. diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx index 57dd3893ad9..031befc14b5 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.tsx @@ -105,7 +105,7 @@ export const Campaigns = () => { { display: ( - + { { - + { el.title } { el.description && ( - + { el.description } ) } @@ -135,7 +135,7 @@ export const Campaigns = () => { } ) } /> { total > perPage && ( - + Date: Thu, 2 Feb 2023 19:06:17 +0800 Subject: [PATCH 142/343] Change Campaigns CSS to use @include font-size(). --- .../marketing/overview-multichannel/Campaigns/Campaigns.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss index 720b6e2ae16..b5c128ae296 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.scss @@ -20,7 +20,7 @@ } &__content-title { - font-size: 13px; + @include font-size( 13 ); font-weight: 600; line-height: 16px; color: $gray-900; From 7bb042fcd22affc206acfb51575b8c12dcf99a12 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Thu, 2 Feb 2023 21:50:36 +0800 Subject: [PATCH 143/343] Simplify CSS for CardHeaderDescription. --- .../CardHeaderDescription/CardHeaderDescription.scss | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/components/CardHeaderDescription/CardHeaderDescription.scss b/plugins/woocommerce-admin/client/marketing/components/CardHeaderDescription/CardHeaderDescription.scss index be1ad094c6e..f6cedefdb70 100644 --- a/plugins/woocommerce-admin/client/marketing/components/CardHeaderDescription/CardHeaderDescription.scss +++ b/plugins/woocommerce-admin/client/marketing/components/CardHeaderDescription/CardHeaderDescription.scss @@ -1,6 +1,4 @@ .woocommerce-marketing-card-header-description { - font-size: 14px; - font-weight: 400; - line-height: 18px; + @include font-size( 14 ); color: $gray-700; } From 54de988f25f6ae0654388888ae9a8fa0e6608863 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Thu, 2 Feb 2023 16:08:44 -0800 Subject: [PATCH 144/343] Update labels for weight units --- ...ass-wc-product-csv-importer-controller.php | 28 ++++++++++--------- .../views/html-product-data-shipping.php | 8 +++++- .../meta-boxes/views/html-variation-admin.php | 6 ++-- .../admin/views/html-bulk-edit-product.php | 18 +++++++++++- .../export/class-wc-product-csv-exporter.php | 13 ++++++--- .../class-wc-rest-products-v1-controller.php | 22 ++++++++------- ...-rest-product-variations-v2-controller.php | 14 ++++++---- .../class-wc-rest-products-v2-controller.php | 14 ++++++---- ...-wc-rest-product-variations-controller.php | 14 ++++++---- .../class-wc-rest-products-controller.php | 14 ++++++---- 10 files changed, 96 insertions(+), 55 deletions(-) diff --git a/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php index a2e69186818..485d6f6ef68 100644 --- a/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php +++ b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php @@ -5,6 +5,8 @@ * @package WooCommerce\Admin\Importers */ +use Automattic\WooCommerce\Utilities\I18nUtil; + if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -482,8 +484,8 @@ class WC_Product_CSV_Importer_Controller { * @return array */ protected function auto_map_columns( $raw_headers, $num_indexes = true ) { - $weight_unit = get_option( 'woocommerce_weight_unit' ); - $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $weight_unit_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) ); + $dimension_unit_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit', 'cm' ) ); /* * @hooked wc_importer_generic_mappings - 10 @@ -513,13 +515,13 @@ class WC_Product_CSV_Importer_Controller { __( 'Low stock amount', 'woocommerce' ) => 'low_stock_amount', __( 'Sold individually?', 'woocommerce' ) => 'sold_individually', /* translators: %s: Weight unit */ - sprintf( __( 'Weight (%s)', 'woocommerce' ), $weight_unit ) => 'weight', + sprintf( __( 'Weight (%s)', 'woocommerce' ), $weight_unit_label ) => 'weight', /* translators: %s: Length unit */ - sprintf( __( 'Length (%s)', 'woocommerce' ), $dimension_unit ) => 'length', + sprintf( __( 'Length (%s)', 'woocommerce' ), $dimension_unit_label ) => 'length', /* translators: %s: Width unit */ - sprintf( __( 'Width (%s)', 'woocommerce' ), $dimension_unit ) => 'width', + sprintf( __( 'Width (%s)', 'woocommerce' ), $dimension_unit_label ) => 'width', /* translators: %s: Height unit */ - sprintf( __( 'Height (%s)', 'woocommerce' ), $dimension_unit ) => 'height', + sprintf( __( 'Height (%s)', 'woocommerce' ), $dimension_unit_label ) => 'height', __( 'Allow customer reviews?', 'woocommerce' ) => 'reviews_allowed', __( 'Purchase note', 'woocommerce' ) => 'purchase_note', __( 'Sale price', 'woocommerce' ) => 'sale_price', @@ -655,9 +657,9 @@ class WC_Product_CSV_Importer_Controller { $meta = str_replace( 'meta:', '', $item ); // Available options. - $weight_unit = get_option( 'woocommerce_weight_unit' ); - $dimension_unit = get_option( 'woocommerce_dimension_unit' ); - $options = array( + $weight_unit_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) ); + $dimension_unit_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit', 'cm' ) ); + $options = array( 'id' => __( 'ID', 'woocommerce' ), 'type' => __( 'Type', 'woocommerce' ), 'sku' => __( 'SKU', 'woocommerce' ), @@ -684,16 +686,16 @@ class WC_Product_CSV_Importer_Controller { 'low_stock_amount' => __( 'Low stock amount', 'woocommerce' ), 'sold_individually' => __( 'Sold individually?', 'woocommerce' ), /* translators: %s: weight unit */ - 'weight' => sprintf( __( 'Weight (%s)', 'woocommerce' ), $weight_unit ), + 'weight' => sprintf( __( 'Weight (%s)', 'woocommerce' ), $weight_unit_label ), 'dimensions' => array( 'name' => __( 'Dimensions', 'woocommerce' ), 'options' => array( /* translators: %s: dimension unit */ - 'length' => sprintf( __( 'Length (%s)', 'woocommerce' ), $dimension_unit ), + 'length' => sprintf( __( 'Length (%s)', 'woocommerce' ), $dimension_unit_label ), /* translators: %s: dimension unit */ - 'width' => sprintf( __( 'Width (%s)', 'woocommerce' ), $dimension_unit ), + 'width' => sprintf( __( 'Width (%s)', 'woocommerce' ), $dimension_unit_label ), /* translators: %s: dimension unit */ - 'height' => sprintf( __( 'Height (%s)', 'woocommerce' ), $dimension_unit ), + 'height' => sprintf( __( 'Height (%s)', 'woocommerce' ), $dimension_unit_label ), ), ), 'category_ids' => __( 'Categories', 'woocommerce' ), diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-shipping.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-shipping.php index 10ba6b29506..6e4b5c3b6f4 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-shipping.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-shipping.php @@ -1,4 +1,6 @@ '_weight', 'value' => $product_object->get_weight( 'edit' ), - 'label' => __( 'Weight', 'woocommerce' ) . ' (' . get_option( 'woocommerce_weight_unit' ) . ')', + 'label' => sprintf( + /* translators: %s: Weight unit */ + __( 'Weight (%s)', 'woocommerce' ), + I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) ) + ), 'placeholder' => wc_format_localized_decimal( 0 ), 'desc_tip' => true, 'description' => __( 'Weight in decimal form', 'woocommerce' ), diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php index 45202b0e407..3ad296f21b4 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php @@ -9,6 +9,8 @@ * @var array $variation_data array of variation data @deprecated 4.4.0. */ +use Automattic\WooCommerce\Utilities\I18nUtil; + defined( 'ABSPATH' ) || exit; ?> @@ -279,9 +281,9 @@ defined( 'ABSPATH' ) || exit; if ( wc_product_weight_enabled() ) { $label = sprintf( - /* translators: %s: weight unit */ + /* translators: %s: Weight unit */ __( 'Weight (%s)', 'woocommerce' ), - esc_html( get_option( 'woocommerce_weight_unit' ) ) + I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) ) ); woocommerce_wp_text_input( diff --git a/plugins/woocommerce/includes/admin/views/html-bulk-edit-product.php b/plugins/woocommerce/includes/admin/views/html-bulk-edit-product.php index 5e81412b5bd..0f182a0f8a7 100644 --- a/plugins/woocommerce/includes/admin/views/html-bulk-edit-product.php +++ b/plugins/woocommerce/includes/admin/views/html-bulk-edit-product.php @@ -3,6 +3,8 @@ * Admin View: Bulk Edit Products */ +use Automattic\WooCommerce\Utilities\I18nUtil; + if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } @@ -131,7 +133,21 @@ if ( ! defined( 'ABSPATH' ) ) { diff --git a/plugins/woocommerce/includes/export/class-wc-product-csv-exporter.php b/plugins/woocommerce/includes/export/class-wc-product-csv-exporter.php index 30f6b096ff5..eff11143cc3 100644 --- a/plugins/woocommerce/includes/export/class-wc-product-csv-exporter.php +++ b/plugins/woocommerce/includes/export/class-wc-product-csv-exporter.php @@ -6,6 +6,8 @@ * @version 3.1.0 */ +use Automattic\WooCommerce\Utilities\I18nUtil; + if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -99,6 +101,9 @@ class WC_Product_CSV_Exporter extends WC_CSV_Batch_Exporter { * @return array */ public function get_default_column_names() { + $weight_unit_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) ); + $dimension_unit_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit', 'cm' ) ); + return apply_filters( "woocommerce_product_export_{$this->export_type}_default_columns", array( @@ -121,13 +126,13 @@ class WC_Product_CSV_Exporter extends WC_CSV_Batch_Exporter { 'backorders' => __( 'Backorders allowed?', 'woocommerce' ), 'sold_individually' => __( 'Sold individually?', 'woocommerce' ), /* translators: %s: weight */ - 'weight' => sprintf( __( 'Weight (%s)', 'woocommerce' ), get_option( 'woocommerce_weight_unit' ) ), + 'weight' => sprintf( __( 'Weight (%s)', 'woocommerce' ), $weight_unit_label ), /* translators: %s: length */ - 'length' => sprintf( __( 'Length (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'length' => sprintf( __( 'Length (%s)', 'woocommerce' ), $dimension_unit_label ), /* translators: %s: width */ - 'width' => sprintf( __( 'Width (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'width' => sprintf( __( 'Width (%s)', 'woocommerce' ), $dimension_unit_label ), /* translators: %s: Height */ - 'height' => sprintf( __( 'Height (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'height' => sprintf( __( 'Height (%s)', 'woocommerce' ), $dimension_unit_label ), 'reviews_allowed' => __( 'Allow customer reviews?', 'woocommerce' ), 'purchase_note' => __( 'Purchase note', 'woocommerce' ), 'sale_price' => __( 'Sale price', 'woocommerce' ), diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php index 64e9442e584..276478ccb2b 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php @@ -10,6 +10,8 @@ * @since 3.0.0 */ +use Automattic\WooCommerce\Utilities\I18nUtil; + if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -1728,8 +1730,8 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { * @return array */ public function get_item_schema() { - $weight_unit = get_option( 'woocommerce_weight_unit' ); - $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $weight_unit_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) ); + $dimension_unit_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit', 'cm' ) ); $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, @@ -1985,7 +1987,7 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { ), 'weight' => array( /* translators: %s: weight unit */ - 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), + 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), @@ -1996,19 +1998,19 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { 'properties' => array( 'length' => array( /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ), + 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'width' => array( /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), + 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'height' => array( /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ), + 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), @@ -2438,7 +2440,7 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { ), 'weight' => array( /* translators: %s: weight unit */ - 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ), + 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), @@ -2449,19 +2451,19 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { 'properties' => array( 'length' => array( /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ), + 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'width' => array( /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ), + 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'height' => array( /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ), + 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php index 3047e3f9bbf..41ce161f808 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php @@ -8,6 +8,8 @@ * @since 3.0.0 */ +use Automattic\WooCommerce\Utilities\I18nUtil; + defined( 'ABSPATH' ) || exit; /** @@ -640,8 +642,8 @@ class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Contr * @return array */ public function get_item_schema() { - $weight_unit = get_option( 'woocommerce_weight_unit' ); - $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $weight_unit_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) ); + $dimension_unit_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit', 'cm' ) ); $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, @@ -835,7 +837,7 @@ class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Contr ), 'weight' => array( /* translators: %s: weight unit */ - 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ), + 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), @@ -846,19 +848,19 @@ class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Contr 'properties' => array( 'length' => array( /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ), + 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'width' => array( /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ), + 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'height' => array( /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ), + 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php index b85486ece55..b1275bd021d 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php @@ -8,6 +8,8 @@ * @since 2.6.0 */ +use Automattic\WooCommerce\Utilities\I18nUtil; + defined( 'ABSPATH' ) || exit; /** @@ -1663,8 +1665,8 @@ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller { * @return array */ public function get_item_schema() { - $weight_unit = get_option( 'woocommerce_weight_unit' ); - $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $weight_unit_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) ); + $dimension_unit_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit', 'cm' ) ); $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, @@ -1935,7 +1937,7 @@ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller { ), 'weight' => array( /* translators: %s: weight unit */ - 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), + 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), @@ -1946,19 +1948,19 @@ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller { 'properties' => array( 'length' => array( /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ), + 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'width' => array( /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), + 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'height' => array( /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ), + 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php index da5ba612522..e0bab404006 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php @@ -8,6 +8,8 @@ * @since 3.0.0 */ +use Automattic\WooCommerce\Utilities\I18nUtil; + defined( 'ABSPATH' ) || exit; use Automattic\Jetpack\Constants; @@ -441,8 +443,8 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V * @return array */ public function get_item_schema() { - $weight_unit = get_option( 'woocommerce_weight_unit' ); - $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $weight_unit_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) ); + $dimension_unit_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit', 'cm' ) ); $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, @@ -643,7 +645,7 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V ), 'weight' => array( /* translators: %s: weight unit */ - 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ), + 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), @@ -654,19 +656,19 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V 'properties' => array( 'length' => array( /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ), + 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'width' => array( /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ), + 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'height' => array( /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ), + 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php index f7d8ff984be..dbcc4a47bb0 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php @@ -8,6 +8,8 @@ * @since 2.6.0 */ +use Automattic\WooCommerce\Utilities\I18nUtil; + defined( 'ABSPATH' ) || exit; /** @@ -804,8 +806,8 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller { * @return array */ public function get_item_schema() { - $weight_unit = get_option( 'woocommerce_weight_unit' ); - $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $weight_unit_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) ); + $dimension_unit_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit', 'cm' ) ); $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, @@ -1080,7 +1082,7 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller { ), 'weight' => array( /* translators: %s: weight unit */ - 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), + 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), @@ -1091,19 +1093,19 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller { 'properties' => array( 'length' => array( /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ), + 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'width' => array( /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), + 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'height' => array( /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ), + 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit_label ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), From ad561d81bf4c3a1a93935966d5922b83c57b1fa3 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Thu, 2 Feb 2023 16:33:43 -0800 Subject: [PATCH 145/343] Update remaining labels for dimensions units --- .../views/html-product-data-shipping.php | 11 +++++-- .../meta-boxes/views/html-variation-admin.php | 4 +-- .../admin/views/html-bulk-edit-product.php | 30 +++++++++++++++++-- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-shipping.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-shipping.php index 6e4b5c3b6f4..9dd7afd74b4 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-shipping.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-shipping.php @@ -30,8 +30,15 @@ if ( ! defined( 'ABSPATH' ) ) { if ( wc_product_dimensions_enabled() ) { ?>

- - + diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php index 3ad296f21b4..2ff898c6bbf 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php @@ -312,9 +312,9 @@ defined( 'ABSPATH' ) || exit; diff --git a/plugins/woocommerce/includes/admin/views/html-bulk-edit-product.php b/plugins/woocommerce/includes/admin/views/html-bulk-edit-product.php index 0f182a0f8a7..4bb918d477d 100644 --- a/plugins/woocommerce/includes/admin/views/html-bulk-edit-product.php +++ b/plugins/woocommerce/includes/admin/views/html-bulk-edit-product.php @@ -171,9 +171,33 @@ if ( ! defined( 'ABSPATH' ) ) { From a42eede13adb2535116fac7864e130c2c5ca9354 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Thu, 2 Feb 2023 16:51:50 -0800 Subject: [PATCH 146/343] Add unit tests --- .../tests/php/src/Utilities/I18nUtilTest.php | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 plugins/woocommerce/tests/php/src/Utilities/I18nUtilTest.php diff --git a/plugins/woocommerce/tests/php/src/Utilities/I18nUtilTest.php b/plugins/woocommerce/tests/php/src/Utilities/I18nUtilTest.php new file mode 100644 index 00000000000..56e11e6d4e2 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Utilities/I18nUtilTest.php @@ -0,0 +1,51 @@ +assertEquals( $expected, $actual ); + } + + /** + * @testdox `get_weight_unit_label` should return an empty string when the input is an invalid weight unit. + */ + public function test_get_invalid_weight_unit_label() { + $actual = I18nUtil::get_weight_unit_label( 'chz' ); // Cheezeburgers. + $expected = ''; + + $this->assertEquals( $expected, $actual ); + } + + /** + * @testdox `get_dimensions_unit_label` should return the input when it's a valid dimensions unit and the locale is en_US. + */ + public function test_get_valid_dimensions_unit_label() { + $actual = I18nUtil::get_dimensions_unit_label( 'yd' ); + $expected = 'yd'; + + $this->assertEquals( $expected, $actual ); + } + + /** + * @testdox `get_dimensions_unit_label` should return an empty string when the input is an invalid dimensions unit. + */ + public function test_get_invalid_dimensions_unit_label() { + $actual = I18nUtil::get_weight_unit_label( 'pc' ); // Parsecs. + $expected = ''; + + $this->assertEquals( $expected, $actual ); + } +} From c678ce72486efecd0ca88bbf909c374e0284fe44 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Fri, 3 Feb 2023 14:14:26 +0530 Subject: [PATCH 147/343] Remove get_from_datastore as it was not being used. --- plugins/woocommerce/src/Caches/OrderCache.php | 12 ------- .../woocommerce/src/Caching/ObjectCache.php | 12 +------ .../tests/php/src/Caching/ObjectCacheTest.php | 32 ------------------- 3 files changed, 1 insertion(+), 55 deletions(-) diff --git a/plugins/woocommerce/src/Caches/OrderCache.php b/plugins/woocommerce/src/Caches/OrderCache.php index e64d55e9ed6..6e2b8d3c16f 100644 --- a/plugins/woocommerce/src/Caches/OrderCache.php +++ b/plugins/woocommerce/src/Caches/OrderCache.php @@ -41,16 +41,4 @@ class OrderCache extends ObjectCache { return null; } - - /** - * Get an object from an authoritative data store. - * This is used by 'get' if the object isn't cached and no custom object retrieval callback is suupplied. - * - * @param int|string $id The id of the object to get. - * - * @return array|object|null The retrieved object, or null if it's not possible to retrieve an object by the given id. - */ - protected function get_from_datastore( $id ) { - return null; - } } diff --git a/plugins/woocommerce/src/Caching/ObjectCache.php b/plugins/woocommerce/src/Caching/ObjectCache.php index 8fc5acfbafe..925509d17b4 100644 --- a/plugins/woocommerce/src/Caching/ObjectCache.php +++ b/plugins/woocommerce/src/Caching/ObjectCache.php @@ -235,10 +235,9 @@ abstract class ObjectCache { $data = $this->get_cache_engine()->get_cached_object( $id, $this->get_object_type() ); if ( null === $data ) { + $object = null; if ( $get_from_datastore_callback ) { $object = $get_from_datastore_callback( $id ); - } else { - $object = $this->get_from_datastore( $id ); } if ( null === $object ) { @@ -297,15 +296,6 @@ abstract class ObjectCache { */ abstract protected function validate( $object ): ?array; - /** - * Get an object from an authoritative data store. - * This is used by 'get' if the object isn't cached and no custom object retrieval callback is suupplied. - * - * @param int|string $id The id of the object to get. - * @return array|object|null The retrieved object, or null if it's not possible to retrieve an object by the given id. - */ - abstract protected function get_from_datastore( $id ); - /** * Get the instance of the cache engine to use. * diff --git a/plugins/woocommerce/tests/php/src/Caching/ObjectCacheTest.php b/plugins/woocommerce/tests/php/src/Caching/ObjectCacheTest.php index 2c59cc51220..66c91c92c0c 100644 --- a/plugins/woocommerce/tests/php/src/Caching/ObjectCacheTest.php +++ b/plugins/woocommerce/tests/php/src/Caching/ObjectCacheTest.php @@ -426,38 +426,6 @@ class ObjectCacheTest extends \WC_Unit_Test_Case { $this->assertEquals( $expected, $this->sut->get( 'the_id' ) ); } - /** - * @testdox 'get' uses the 'get_from_datastore' method if there's no object cached under the passed id, and caches the object retrieved. - */ - public function test_try_getting_not_cached_object_get_from_datastore_implemented() { - // phpcs:disable Squiz.Commenting - - $sut = new class() extends ObjectCache { - public function get_object_type(): string { - return 'the_type'; - } - - protected function get_from_datastore( $id ) { - return array( 'id' => $id ); - } - - protected function get_object_id( $object ) { - } - - protected function validate( $object ) : ?array { - return null; - } - }; - - // phpcs:enable Squiz.Commenting - - $result = $sut->get( 'the_id' ); - - $expected = array( 'id' => 'the_id' ); - $this->assertEquals( $expected, $result ); - $this->assertEquals( $expected, $sut->get( 'the_id' ) ); - } - /** * @testdox 'remove' removes a cached object and returns true, or returns false if there's no cached object under the passed id. */ From b5ae765b9311a88d94b24154358fbdde59e872d6 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Fri, 3 Feb 2023 14:17:54 +0530 Subject: [PATCH 148/343] Use correct class name. --- plugins/woocommerce/src/Caches/OrderCache.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce/src/Caches/OrderCache.php b/plugins/woocommerce/src/Caches/OrderCache.php index 6e2b8d3c16f..6f49fa308dd 100644 --- a/plugins/woocommerce/src/Caches/OrderCache.php +++ b/plugins/woocommerce/src/Caches/OrderCache.php @@ -35,8 +35,8 @@ class OrderCache extends ObjectCache { * @return string[]|null An array of error messages, or null if the object is valid. */ protected function validate( $object ): ?array { - if ( ! $object instanceof \WC_Abstract_Legacy_Order ) { - return array( 'The supplied order is not an instance of WC_Order, ' . gettype( $object ) ); + if ( ! $object instanceof \WC_Abstract_Order ) { + return array( 'The supplied order is not an instance of WC_Abstract_Order, ' . gettype( $object ) ); } return null; From 3828aabbef631b8e6ce1778ef8a1cee80e60b83f Mon Sep 17 00:00:00 2001 From: Kevin Ruscoe Date: Fri, 3 Feb 2023 09:54:45 +0000 Subject: [PATCH 149/343] clean up sass variable names (#28908) --- .../woocommerce/changelog/enhancement-clean-up-of-sass-vars | 4 ++++ plugins/woocommerce/client/legacy/css/_mixins.scss | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/enhancement-clean-up-of-sass-vars diff --git a/plugins/woocommerce/changelog/enhancement-clean-up-of-sass-vars b/plugins/woocommerce/changelog/enhancement-clean-up-of-sass-vars new file mode 100644 index 00000000000..ba11259b87c --- /dev/null +++ b/plugins/woocommerce/changelog/enhancement-clean-up-of-sass-vars @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Change the sass variable names to more predictable ones. diff --git a/plugins/woocommerce/client/legacy/css/_mixins.scss b/plugins/woocommerce/client/legacy/css/_mixins.scss index 2827c082325..a585be08544 100644 --- a/plugins/woocommerce/client/legacy/css/_mixins.scss +++ b/plugins/woocommerce/client/legacy/css/_mixins.scss @@ -293,7 +293,7 @@ } mark.yes { - color: $green; + color: var(--wc-green); } mark.no { From 659566ed0dd5b67a0e79c75000cff5ac15205148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Fri, 3 Feb 2023 10:57:34 +0100 Subject: [PATCH 150/343] Fix variable products options and add to cart form not wrapping on different lines --- .../legacy/css/twenty-twenty-three.scss | 4 --- .../client/legacy/css/twenty-twenty-two.scss | 6 ++++ .../legacy/css/woocommerce-blocktheme.scss | 34 +++++++++++-------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss index 54a4546b2f6..0a3d6bad802 100644 --- a/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss +++ b/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss @@ -315,10 +315,6 @@ font-size: 1em; } } - - .quantity { - display: inline-block; - } } table.variations tr { diff --git a/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss index 8d5113a7445..ebe82fd8cf7 100644 --- a/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss +++ b/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss @@ -328,6 +328,12 @@ $tt2-gray: #f7f7f7; } } + button[name="add-to-cart"], + button.single_add_to_cart_button { + margin-top: 0.5rem; + margin-bottom: var(--wp--style--block-gap); + } + ol.flex-control-thumbs { padding-left: 0; float: left; diff --git a/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss b/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss index 5b57f94b72d..2e17e871a9a 100644 --- a/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss +++ b/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss @@ -100,22 +100,26 @@ } } - .cart { - display: flex; - align-items: center; - } - - .quantity { - // Adjust positioning of quantity selector and button. - .qty { - margin-right: 0.5rem; + form.cart { + div.quantity { + display: inline-block; + float: none; // Remove float set by WC core. + vertical-align: middle; + + // Adjust positioning of quantity selector and button. + .qty { + margin-right: 0.5rem; + } + } + + button[name="add-to-cart"], + button.single_add_to_cart_button { + display: inline-block; + float: none; // Remove float set by WC core. + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; } - } - - button[name="add-to-cart"], - button.single_add_to_cart_button { - margin-top: 0; - margin-bottom: 0; } .related.products { From 4971eaa350a30a7ddbc3e18e1e5198200f526d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Fri, 3 Feb 2023 11:33:03 +0100 Subject: [PATCH 151/343] Increase TT2 styles specificity so they aren't overridden by block theme styles --- plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss index ebe82fd8cf7..7d214e67fc4 100644 --- a/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss +++ b/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss @@ -328,8 +328,9 @@ $tt2-gray: #f7f7f7; } } - button[name="add-to-cart"], - button.single_add_to_cart_button { + form.cart button[name="add-to-cart"], + form.cart button.single_add_to_cart_button { + display: block; margin-top: 0.5rem; margin-bottom: var(--wp--style--block-gap); } From d525a56f6846cfbaca6ca60780c45bbf828822dc Mon Sep 17 00:00:00 2001 From: Nazmul Hasan Date: Mon, 6 Feb 2023 05:03:59 +0800 Subject: [PATCH 152/343] Fix Typo in Variable Name on line 342 I've noticed on line 339 has an empty array ($attributes) which might be intended to use as a fallback variable for line 345. The array ($attributes) is then extended on line 342, but the variable name at this point is $attribute instead $attributes --- plugins/woocommerce/includes/wc-product-functions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce/includes/wc-product-functions.php b/plugins/woocommerce/includes/wc-product-functions.php index d1099b5605c..c6590f62890 100644 --- a/plugins/woocommerce/includes/wc-product-functions.php +++ b/plugins/woocommerce/includes/wc-product-functions.php @@ -339,10 +339,10 @@ function wc_placeholder_img( $size = 'woocommerce_thumbnail', $attr = '' ) { $attributes = array(); foreach ( $attr as $name => $value ) { - $attribute[] = esc_attr( $name ) . '="' . esc_attr( $value ) . '"'; + $attributes[] = esc_attr( $name ) . '="' . esc_attr( $value ) . '"'; } - $image_html = ''; + $image_html = ''; } return apply_filters( 'woocommerce_placeholder_img', $image_html, $size, $dimensions ); From f1c2b28608d7c64aed8a1021cab195a4cee87f91 Mon Sep 17 00:00:00 2001 From: Moon Date: Mon, 6 Feb 2023 18:10:25 -0800 Subject: [PATCH 153/343] Add codistoconnect to pluginNames (#36766) * Add codistoconnect to pluginNames * Add changelog --- .../changelog/fix-36765-add-codistoconnect-to-plugin-names | 4 ++++ packages/js/data/src/plugins/constants.ts | 1 + 2 files changed, 5 insertions(+) create mode 100644 packages/js/data/changelog/fix-36765-add-codistoconnect-to-plugin-names diff --git a/packages/js/data/changelog/fix-36765-add-codistoconnect-to-plugin-names b/packages/js/data/changelog/fix-36765-add-codistoconnect-to-plugin-names new file mode 100644 index 00000000000..2d460863cb2 --- /dev/null +++ b/packages/js/data/changelog/fix-36765-add-codistoconnect-to-plugin-names @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Add codistoconnect to pluginNames \ No newline at end of file diff --git a/packages/js/data/src/plugins/constants.ts b/packages/js/data/src/plugins/constants.ts index fa0b5617e68..a3949810cc2 100644 --- a/packages/js/data/src/plugins/constants.ts +++ b/packages/js/data/src/plugins/constants.ts @@ -60,4 +60,5 @@ export const pluginNames = { 'woocommerce' ), 'tiktok-for-business:alt': __( 'TikTok for WooCommerce', 'woocommerce' ), + codistoconnect: __( 'Omnichannel for WooCommerce', 'woocommerce' ), }; From 35384a4fe62151d9d631ec7be0c8a8c7df5d7da9 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Tue, 7 Feb 2023 11:10:59 +0100 Subject: [PATCH 154/343] Fix decimal points for NOK --- plugins/woocommerce/i18n/locale-info.php | 4 ++-- .../tests/api-core-tests/tests/data/data-crud.test.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/woocommerce/i18n/locale-info.php b/plugins/woocommerce/i18n/locale-info.php index 919cd66999f..f07d2add31f 100644 --- a/plugins/woocommerce/i18n/locale-info.php +++ b/plugins/woocommerce/i18n/locale-info.php @@ -2656,7 +2656,7 @@ return array( 'currency_pos' => 'left_space', 'thousand_sep' => ' ', 'decimal_sep' => ',', - 'num_decimals' => 0, + 'num_decimals' => 2, 'weight_unit' => 'kg', 'dimension_unit' => 'cm', 'direction' => 'ltr', @@ -3200,7 +3200,7 @@ return array( 'currency_pos' => 'left_space', 'thousand_sep' => ' ', 'decimal_sep' => ',', - 'num_decimals' => 0, + 'num_decimals' => 2, 'weight_unit' => 'kg', 'dimension_unit' => 'cm', 'direction' => 'ltr', diff --git a/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js index b977c21fb13..53e530a575b 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js @@ -1903,7 +1903,7 @@ test.describe('Data API tests', () => { "currency_pos": "left_space", "decimal_sep": ",", "dimension_unit": "cm", - "num_decimals": 0, + "num_decimals": 2, "thousand_sep": " ", "weight_unit": "kg", "states": [] @@ -1999,7 +1999,7 @@ test.describe('Data API tests', () => { "currency_pos": "left_space", "decimal_sep": ",", "dimension_unit": "cm", - "num_decimals": 0, + "num_decimals": 2, "thousand_sep": " ", "weight_unit": "kg", "states": [] @@ -3542,7 +3542,7 @@ test.describe('Data API tests', () => { "currency_pos": "left_space", "decimal_sep": ",", "dimension_unit": "cm", - "num_decimals": 0, + "num_decimals": 2, "thousand_sep": " ", "weight_unit": "kg", "states": [] @@ -3638,7 +3638,7 @@ test.describe('Data API tests', () => { "currency_pos": "left_space", "decimal_sep": ",", "dimension_unit": "cm", - "num_decimals": 0, + "num_decimals": 2, "thousand_sep": " ", "weight_unit": "kg", "states": [] From dc616bd4c6d69505a4767d2cccf505ac1b09d623 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Tue, 7 Feb 2023 11:14:40 +0100 Subject: [PATCH 155/343] update changelog --- plugins/woocommerce/changelog/fix-decimal-points-for-nok | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-decimal-points-for-nok diff --git a/plugins/woocommerce/changelog/fix-decimal-points-for-nok b/plugins/woocommerce/changelog/fix-decimal-points-for-nok new file mode 100644 index 00000000000..69ebe08d54f --- /dev/null +++ b/plugins/woocommerce/changelog/fix-decimal-points-for-nok @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix decimal points for NOK currency From c8863ddb3ca83223b7777320782e7eeecb33019d Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Tue, 7 Feb 2023 16:41:49 +0100 Subject: [PATCH 156/343] Incorporated feedback from review --- plugins/woocommerce/package.json | 3 + plugins/woocommerce/tests/e2e-pw/README.md | 146 ++++++++++----------- 2 files changed, 76 insertions(+), 73 deletions(-) diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index fe42c22bc33..fff6b4283d3 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -33,6 +33,9 @@ "env:test": "pnpm run env:dev && ./tests/e2e-pw/bin/test-env-setup.sh", "test:e2e-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/e2e-pw/playwright.config.js", "test:api-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/api-core-tests/playwright.config.js", + "env:start": "pnpm wp-env start", + "env:restart": "pnpm wp-env destroy && pnpm wp-env start", + "env:stop": "pnpm wp-env stop", "env:test:cot": "pnpm run env:dev && ENABLE_HPOS=1 ./tests/e2e-pw/bin/test-env-setup.sh", "env:performance-init": "./tests/performance/bin/init-sample-products.sh", "env:down": "pnpm wp-env stop", diff --git a/plugins/woocommerce/tests/e2e-pw/README.md b/plugins/woocommerce/tests/e2e-pw/README.md index a6813073019..4cb9d6fb2a8 100644 --- a/plugins/woocommerce/tests/e2e-pw/README.md +++ b/plugins/woocommerce/tests/e2e-pw/README.md @@ -4,25 +4,25 @@ This is the documentation for the new e2e testing setup based on Playwright and ## Table of contents -- [Pre-requisites](#pre-requisites) -- [Introduction](#introduction) -- [About the Environment](#about-the-environment) -- [Test Variables](#test-variables) -- [Guide for writing e2e tests](#guide-for-writing-e2e-tests) - - [Tools for writing tests](#tools-for-writing-tests) - - [Creating test structure](#creating-test-structure) - - [Writing the test](#writing-the-test) -- [Guide for using test reports](#guide-for-using-test-reports) - - [Viewing the Playwright HTML report](#viewing-the-playwright-html-report) - - [Viewing the Allure report](#viewing-the-allure-report) -- [Debugging tests](#debugging-tests) +- [Pre-requisites](#pre-requisites) +- [Introduction](#introduction) +- [About the Environment](#about-the-environment) +- [Test Variables](#test-variables) +- [Guide for writing e2e tests](#guide-for-writing-e2e-tests) + - [Tools for writing tests](#tools-for-writing-tests) + - [Creating test structure](#creating-test-structure) + - [Writing the test](#writing-the-test) +- [Guide for using test reports](#guide-for-using-test-reports) + - [Viewing the Playwright HTML report](#viewing-the-playwright-html-report) + - [Viewing the Allure report](#viewing-the-allure-report) +- [Debugging tests](#debugging-tests) ## Pre-requisites -- Node.js ([Installation instructions](https://nodejs.org/en/download/)) -- NVM ([Installation instructions](https://github.com/nvm-sh/nvm)) -- PNPM ([Installation instructions](https://pnpm.io/installation)) -- Docker and Docker Compose ([Installation instructions](https://docs.docker.com/engine/install/)) +- Node.js ([Installation instructions](https://nodejs.org/en/download/)) +- NVM ([Installation instructions](https://github.com/nvm-sh/nvm)) +- PNPM ([Installation instructions](https://pnpm.io/installation)) +- Docker and Docker Compose ([Installation instructions](https://docs.docker.com/engine/install/)) Note, that if you are on Mac and you install docker through other methods such as homebrew, for example, your steps to set it up might be different. The commands listed in steps below may also vary. @@ -34,22 +34,22 @@ End-to-end tests are powered by Playwright. The test site is spinned up using `w **Running tests for the first time:** -- `nvm use` -- `pnpm install` -- `pnpm run build --filter=woocommerce` -- `pnpm env:test --filter=woocommerce` +- `nvm use` +- `pnpm install` +- `pnpm run build --filter=woocommerce` +- `pnpm env:start` To run the test again, re-create the environment to start with a fresh state: -- `pnpm env:destroy --filter=woocommerce` -- `pnpm env:test --filter=woocommerce` +- `pnpm restart` +- `pnpm test:e2e-pw` Other ways of running tests: -- `pnpm env:test --filter=woocommerce` (headless) -- `cd plugin/woocommerce && USE_WP_ENV=1 pnpm playwright test --config=tests/e2e-pw/playwright.config.js --headed` (headed) -- `cd plugins/woocommerce && USE_WP_ENV=1 pnpm playwright test --config=tests/e2e-pw/playwright.config.js --debug` (debug) -- `cd plugins/woocommerce && USE_WP_ENV=1 pnpm playwright test --config=tests/e2e-pw/playwright.config.js ./tests/e2e-pw/tests/activate-and-setup/basic-setup.spec.js` (running a single test) +- `pnpm env:test` (headless) +- `cd plugin/woocommerce && USE_WP_ENV=1 pnpm playwright test --config=tests/e2e-pw/playwright.config.js --headed` (headed) +- `cd plugins/woocommerce && USE_WP_ENV=1 pnpm playwright test --config=tests/e2e-pw/playwright.config.js --debug` (debug) +- `cd plugins/woocommerce && USE_WP_ENV=1 pnpm playwright test --config=tests/e2e-pw/playwright.config.js ./tests/e2e-pw/tests/activate-and-setup/basic-setup.spec.js` (running a single test) To see all options, run `cd plugins/woocommerce && pnpm playwright test --help` @@ -57,15 +57,14 @@ To see all options, run `cd plugins/woocommerce && pnpm playwright test --help` The default values are: -- Latest stable WordPress version -- PHP 7.4 -- MariaDB -- URL: `http://localhost:8086/` -- Admin credentials: `admin/password` +- Latest stable WordPress version +- PHP 7.4 +- MariaDB +- URL: `http://localhost:8086/` +- Admin credentials: `admin/password` If you want to customize these, check the [Test Variables](#test-variables) section. - For more information how to configure the test environment for `wp-env`, please checkout the [documentation](https://github.com/WordPress/gutenberg/tree/trunk/packages/env) documentation. ### Test Variables @@ -73,18 +72,18 @@ For more information how to configure the test environment for `wp-env`, please The test environment uses the following test variables: ```json -{ - "url": "http://localhost:8086/", - "users": { - "admin": { - "username": "admin", - "password": "password" - }, - "customer": { - "username": "customer", - "password": "password" - } - } +{ + "url": "http://localhost:8086/", + "users": { + "admin": { + "username": "admin", + "password": "password" + }, + "customer": { + "username": "customer", + "password": "password" + } + } } ``` @@ -94,16 +93,18 @@ If you need to modify the port for your local test environment (eg. port is alre Edit [.wp-env.json](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/.wp-env.json) and [playwright.config.js](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/e2e/playwright.config.js). -**Modiify port for e2e-environment** +**Modify port for e2e-environment** -Edit [tests/e2e/config/default.json](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/e2e/config/default.json).**** +Edit [tests/e2e/config/default.json](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/e2e/config/default.json).\*\*\*\* ### Starting/stopping the environment After you run a test, it's best to restart the environment to start from a fresh state. We are currently working to reset the state more efficiently to avoid the restart being needed, but this is a work-in-progress. -- `pnpm env:down --filter=woocommerce` to stop the environment -- `pnpm env:destroy --filter=woocommerce` when you make changes to `.wp-env.json` +- `pnpm env:stop` to stop the environment +- `pnpm env:destroy` when you make changes to `.wp-env.json` +- `pnpm env:start` to start the environment +- `pnpm env:restart` to stop/destroy and then start the environment (useful for re-testing) ## Guide for writing e2e tests @@ -111,10 +112,10 @@ After you run a test, it's best to restart the environment to start from a fresh It is a good practice to start working on the test by identifying what needs to be tested on the higher and lower levels. For example, if you are writing a test to verify that merchant can create a virtual product, the overview of the test will be as follows: -- Merchant can create virtual product - - Merchant can log in - - Merchant can create virtual product - - Merchant can verify that virtual product was created +- Merchant can create virtual product + - Merchant can log in + - Merchant can create virtual product + - Merchant can verify that virtual product was created Once you identify the structure of the test, you can move on to writing it. @@ -122,41 +123,36 @@ Once you identify the structure of the test, you can move on to writing it. The structure of the test serves as a skeleton for the test itself. You can turn it into a test by using `describe()` and `it()` methods of Playwright: -- [`test.describe()`](https://playwright.dev/docs/api/class-test#test-describe) - creates a block that groups together several related tests; -- [`test()`](https://playwright.dev/docs/api/class-test#test-call) - actual method that runs the test. +- [`test.describe()`](https://playwright.dev/docs/api/class-test#test-describe) - creates a block that groups together several related tests; +- [`test()`](https://playwright.dev/docs/api/class-test#test-call) - actual method that runs the test. Based on our example, the test skeleton would look as follows: ```js test.describe( 'Merchant can create virtual product', () => { - test( 'merchant can log in', async () => { + test( 'merchant can log in', async () => {} ); - } ); + test( 'merchant can create virtual product', async () => {} ); - test( 'merchant can create virtual product', async () => { - - } ); - - test( 'merchant can verify that virtual product was created', async () => { - - } ); + test( 'merchant can verify that virtual product was created', async () => {} ); } ); ``` ## Guide for using test reports The tests would generate three kinds of reports after the run: + 1. A Playwright HTML report. 1. A Playwright JSON report. 1. Allure results. By default, they are saved inside the `test-results` folder. If you want to save them in a custom location, just assign the absolute path to the environment variables mentioned in the [Playwright](https://playwright.dev/docs/test-reporters) and [Allure-Playwright](https://www.npmjs.com/package/allure-playwright) documentation. -| Report | Default location | Environment variable for custom location | -| ----------- | ---------------- | ---------------------------------------- | -| Playwright HTML report | `test-results/playwright-report` | `PLAYWRIGHT_HTML_REPORT` | -| Playwright JSON report | `test-results/test-results.json` | `PLAYWRIGHT_JSON_OUTPUT_NAME` | -| Allure results | `test-results/allure-results` | `ALLURE_RESULTS_DIR` | +| Report | Default location | Environment variable for custom location | +| ---------------------- | -------------------------------- | ---------------------------------------- | +| Playwright HTML report | `test-results/playwright-report` | `PLAYWRIGHT_HTML_REPORT` | +| Playwright JSON report | `test-results/test-results.json` | `PLAYWRIGHT_JSON_OUTPUT_NAME` | +| Allure results | `test-results/allure-results` | `ALLURE_RESULTS_DIR` | ### Viewing the Playwright HTML report @@ -172,13 +168,15 @@ For more details about the Playwright HTML report, see their [HTML Reporter](htt ### Viewing the Allure report This assumes that you're already familiar with reports generated by the [Allure Framework](https://github.com/allure-framework), particularly: -- What the `allure-results` and `allure-report` folders are, and how they're different from each other. -- Allure commands like `allure generate` and `allure open`. + +- What the `allure-results` and `allure-report` folders are, and how they're different from each other. +- Allure commands like `allure generate` and `allure open`. Use the `allure generate` command to generate an HTML report from the `allure-results` directory created at the end of the test run. Then, use the `allure open` command to open it on your browser. For example, assuming that: -- You're at the root of the WooCommerce monorepo -- You did not specify a custom location for `allure-results` (you did not assign a value to `ALLURE_RESULTS_DIR`) -- You want to generate the `allure-report` folder in `plugins/woocommerce/tests/e2e-pw/test-results` + +- You're at the root of the WooCommerce monorepo +- You did not specify a custom location for `allure-results` (you did not assign a value to `ALLURE_RESULTS_DIR`) +- You want to generate the `allure-report` folder in `plugins/woocommerce/tests/e2e-pw/test-results` Then you would need to use the following commands: @@ -191,12 +189,14 @@ pnpm exec allure open tests/e2e-pw/test-results/allure-report A browser window should open the Allure report. If you're on [WSL](https://learn.microsoft.com/en-us/windows/wsl/about) however, you might get this message right after running the `allure open` command: + ``` Starting web server... 2022-12-09 18:52:01.323:INFO::main: Logging initialized @286ms to org.eclipse.jetty.util.log.StdErrLog Can not open browser because this capability is not supported on your platform. You can use the link below to open the report manually. Server started at . Press to exit ``` + In this case, take note of the port number (38917 in the example above) and then use it to navigate to `http://localhost`. Taking the example above, you should be able to view the Allure report on http://localhost:38917. To know more about the allure-playwright integration, see their [GitHub documentation](https://github.com/allure-framework/allure-js/tree/master/packages/allure-playwright). From 3739c0869a32b73cc0b697f746af1f5f518df32a Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Wed, 8 Feb 2023 10:31:16 +0100 Subject: [PATCH 157/343] Need to run config after starting --- plugins/woocommerce/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index fff6b4283d3..479a4f79da8 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -33,8 +33,8 @@ "env:test": "pnpm run env:dev && ./tests/e2e-pw/bin/test-env-setup.sh", "test:e2e-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/e2e-pw/playwright.config.js", "test:api-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/api-core-tests/playwright.config.js", - "env:start": "pnpm wp-env start", - "env:restart": "pnpm wp-env destroy && pnpm wp-env start", + "env:start": "pnpm wp-env start && ./tests/e2e-pw/bin/test-env-setup.sh", + "env:restart": "pnpm wp-env destroy && pnpm wp-env start && ./tests/e2e-pw/bin/test-env-setup.sh", "env:stop": "pnpm wp-env stop", "env:test:cot": "pnpm run env:dev && ENABLE_HPOS=1 ./tests/e2e-pw/bin/test-env-setup.sh", "env:performance-init": "./tests/performance/bin/init-sample-products.sh", From f9f14f5b00f5447235d62f26d31d78e12a3c2d2d Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Wed, 8 Feb 2023 10:52:54 +0100 Subject: [PATCH 158/343] Update Playwright 1.28.0 -> 1.30.0 --- plugins/woocommerce/package.json | 2 +- pnpm-lock.yaml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index 83a77b28d1e..40ec87707d1 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -50,7 +50,7 @@ "@babel/core": "7.12.9", "@babel/preset-env": "7.12.7", "@babel/register": "7.12.1", - "@playwright/test": "^1.28.0", + "@playwright/test": "^1.30.0", "@typescript-eslint/eslint-plugin": "^5.43.0", "@typescript-eslint/experimental-utils": "^5.43.0", "@typescript-eslint/parser": "^5.43.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 329e506d368..a2fbe6e369f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1359,7 +1359,7 @@ importers: '@babel/core': 7.12.9 '@babel/preset-env': 7.12.7 '@babel/register': 7.12.1 - '@playwright/test': ^1.28.0 + '@playwright/test': ^1.30.0 '@typescript-eslint/eslint-plugin': ^5.43.0 '@typescript-eslint/experimental-utils': ^5.43.0 '@typescript-eslint/parser': ^5.43.0 @@ -1407,7 +1407,7 @@ importers: '@babel/core': 7.12.9 '@babel/preset-env': 7.12.7_@babel+core@7.12.9 '@babel/register': 7.12.1_@babel+core@7.12.9 - '@playwright/test': 1.28.0 + '@playwright/test': 1.30.0 '@typescript-eslint/eslint-plugin': 5.43.0_7f3f6qw7x62gbtvc33ujooybme '@typescript-eslint/experimental-utils': 5.43.0_yygwinqv3a2io74xmwofqb7uka '@typescript-eslint/parser': 5.43.0_yygwinqv3a2io74xmwofqb7uka @@ -9378,13 +9378,13 @@ packages: dependencies: '@octokit/openapi-types': 13.10.0 - /@playwright/test/1.28.0: - resolution: {integrity: sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==} + /@playwright/test/1.30.0: + resolution: {integrity: sha512-SVxkQw1xvn/Wk/EvBnqWIq6NLo1AppwbYOjNLmyU0R1RoQ3rLEBtmjTnElcnz8VEtn11fptj1ECxK0tgURhajw==} engines: {node: '>=14'} hasBin: true dependencies: '@types/node': 17.0.21 - playwright-core: 1.28.0 + playwright-core: 1.30.0 dev: true /@pmmmwh/react-refresh-webpack-plugin/0.5.1_a3gyllrqvxpec3fpybsrposvju: @@ -32717,8 +32717,8 @@ packages: find-up: 5.0.0 dev: true - /playwright-core/1.28.0: - resolution: {integrity: sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==} + /playwright-core/1.30.0: + resolution: {integrity: sha512-7AnRmTCf+GVYhHbLJsGUtskWTE33SwMZkybJ0v6rqR1boxq2x36U7p1vDRV7HO2IwTZgmycracLxPEJI49wu4g==} engines: {node: '>=14'} hasBin: true dev: true From 4927dc03520a204d1dc778b14f4c58cac22a2cdf Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Wed, 8 Feb 2023 10:57:44 +0100 Subject: [PATCH 159/343] Changelog --- plugins/woocommerce/changelog/e2e-playwright-1_3_0 | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/e2e-playwright-1_3_0 diff --git a/plugins/woocommerce/changelog/e2e-playwright-1_3_0 b/plugins/woocommerce/changelog/e2e-playwright-1_3_0 new file mode 100644 index 00000000000..0dc81d9bef6 --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-playwright-1_3_0 @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update Playwright version from 1.28.0 -> 1.30.0 From 5b27b02508cc15e1710263256c73c5ad7372ab27 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Wed, 8 Feb 2023 22:43:08 +0100 Subject: [PATCH 160/343] Fix linting issues --- .../includes/admin/views/html-bulk-edit-product.php | 6 +++--- .../Version1/class-wc-rest-products-v1-controller.php | 2 +- .../class-wc-rest-product-variations-v2-controller.php | 2 +- .../Version2/class-wc-rest-products-v2-controller.php | 2 +- .../class-wc-rest-product-variations-controller.php | 2 +- .../Version3/class-wc-rest-products-controller.php | 2 +- plugins/woocommerce/includes/wc-formatting-functions.php | 4 ++-- plugins/woocommerce/src/Utilities/I18nUtil.php | 9 +++++++-- 8 files changed, 17 insertions(+), 12 deletions(-) diff --git a/plugins/woocommerce/includes/admin/views/html-bulk-edit-product.php b/plugins/woocommerce/includes/admin/views/html-bulk-edit-product.php index 4bb918d477d..fc75c1b6f79 100644 --- a/plugins/woocommerce/includes/admin/views/html-bulk-edit-product.php +++ b/plugins/woocommerce/includes/admin/views/html-bulk-edit-product.php @@ -179,7 +179,7 @@ if ( ! defined( 'ABSPATH' ) ) { name="_length" class="text length" - placeholder="" + placeholder="" value="" > - placeholder="" + placeholder="" value="" > - placeholder="" + placeholder="" value="" > diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php index 276478ccb2b..41cb034cbe9 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php @@ -1732,7 +1732,7 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { public function get_item_schema() { $weight_unit_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) ); $dimension_unit_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit', 'cm' ) ); - $schema = array( + $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php index 41ce161f808..6ea706ff8a9 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php @@ -644,7 +644,7 @@ class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Contr public function get_item_schema() { $weight_unit_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) ); $dimension_unit_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit', 'cm' ) ); - $schema = array( + $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php index b1275bd021d..ffaf3f9d596 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php @@ -1667,7 +1667,7 @@ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller { public function get_item_schema() { $weight_unit_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) ); $dimension_unit_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit', 'cm' ) ); - $schema = array( + $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php index e0bab404006..4c332f2cc11 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php @@ -445,7 +445,7 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V public function get_item_schema() { $weight_unit_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) ); $dimension_unit_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit', 'cm' ) ); - $schema = array( + $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php index dbcc4a47bb0..1a61edd6c4d 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php @@ -808,7 +808,7 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller { public function get_item_schema() { $weight_unit_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) ); $dimension_unit_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit', 'cm' ) ); - $schema = array( + $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', diff --git a/plugins/woocommerce/includes/wc-formatting-functions.php b/plugins/woocommerce/includes/wc-formatting-functions.php index a83caeaf519..fd5bf9b7e01 100644 --- a/plugins/woocommerce/includes/wc-formatting-functions.php +++ b/plugins/woocommerce/includes/wc-formatting-functions.php @@ -1307,7 +1307,7 @@ function wc_format_weight( $weight ) { $weight_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit' ) ); $weight_string = sprintf( - // translators: 1. A formatted number; 2. A label for a weight unit of measure. E.g. 2.72 kg + // translators: 1. A formatted number; 2. A label for a weight unit of measure. E.g. 2.72 kg. _x( '%1$s %2$s', 'formatted weight', 'woocommerce' ), $weight_string, $weight_label @@ -1333,7 +1333,7 @@ function wc_format_dimensions( $dimensions ) { $dimension_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit' ) ); $dimension_string = sprintf( - // translators: 1. A formatted number; 2. A label for a dimensions unit of measure. E.g. 3.14 cm + // translators: 1. A formatted number; 2. A label for a dimensions unit of measure. E.g. 3.14 cm. _x( '%1$s %2$s', 'formatted dimensions', 'woocommerce' ), $dimension_string, $dimension_label diff --git a/plugins/woocommerce/src/Utilities/I18nUtil.php b/plugins/woocommerce/src/Utilities/I18nUtil.php index 428790d76b2..07a3480565f 100644 --- a/plugins/woocommerce/src/Utilities/I18nUtil.php +++ b/plugins/woocommerce/src/Utilities/I18nUtil.php @@ -5,8 +5,13 @@ namespace Automattic\WooCommerce\Utilities; +/** + * A class of utilities for dealing with internationalization. + */ final class I18nUtil { /** + * A cache for the i18n units data. + * * @var array $units */ private static $units; @@ -14,7 +19,7 @@ final class I18nUtil { /** * Get the translated label for a weight unit of measure. * - * @param string $weight_unit + * @param string $weight_unit The abbreviated weight unit in English, e.g. kg. * * @return string */ @@ -35,7 +40,7 @@ final class I18nUtil { /** * Get the translated label for a dimensions unit of measure. * - * @param string $dimensions_unit + * @param string $dimensions_unit The abbreviated dimension unit in English, e.g. cm. * * @return string */ From 6ba45889b6847e69bda9bbe7cd53732b55fb578c Mon Sep 17 00:00:00 2001 From: Sam Seay Date: Thu, 9 Feb 2023 22:08:36 +1300 Subject: [PATCH 161/343] Turn on code-cov for PR checking of unit test coverage (#36548) --- .codecov.yml | 61 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/.codecov.yml b/.codecov.yml index bec8da4c270..907ee344cb6 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,27 +1,46 @@ codecov: - notify: - require_ci_to_pass: yes + notify: + require_ci_to_pass: yes + +ignore: + - '**/tests' + - '**/test' + - 'tools/**' + - 'packages/js/admin-e2e-tests' + - 'packages/js/api-core-tests' + - 'packages/js/create-woo-extension' + - 'packages/js/e2e-core-tests' + - 'packages/js/e2e-environment' + - 'packages/js/e2e-utils' + - 'packages/js/eslint-plugin' + - 'packages/js/internal-e2e-builds' + - 'packages/js/internal-js-tests' + - 'packages/js/internal-style-build' + - '**/*.test.*' coverage: - precision: 2 - round: nearest - range: "50...100" - - status: - project: - default: - informational: true - patch: - default: - informational: true - changes: off + precision: 1 + round: nearest + range: '50...80' + status: + project: + default: + target: auto + patch: + default: + target: auto parsers: - gcov: - branch_detection: - conditional: yes - loop: yes - method: no - macro: no + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no -comment: false +comment: + layout: 'reach, diff, flags, files' + behavior: default + require_changes: false + require_base: no + require_head: yes From 73a6475a3d41c069b4bbb630bccdd985203e6b37 Mon Sep 17 00:00:00 2001 From: Ron Rennick Date: Mon, 13 Feb 2023 04:13:57 -0400 Subject: [PATCH 162/343] ensure unit test install ABSPATH is a real path (#36641) Co-authored-by: Ron Rennick --- plugins/woocommerce/changelog/fix-36638 | 4 ++++ plugins/woocommerce/tests/bin/install.sh | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-36638 diff --git a/plugins/woocommerce/changelog/fix-36638 b/plugins/woocommerce/changelog/fix-36638 new file mode 100644 index 00000000000..43df5b4877c --- /dev/null +++ b/plugins/woocommerce/changelog/fix-36638 @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix WordPress unit tests libraries being installed in a symlinked folder structure diff --git a/plugins/woocommerce/tests/bin/install.sh b/plugins/woocommerce/tests/bin/install.sh index 05f7e9065ad..ce8151afde7 100755 --- a/plugins/woocommerce/tests/bin/install.sh +++ b/plugins/woocommerce/tests/bin/install.sh @@ -126,7 +126,7 @@ install_test_suite() { if [ ! -f wp-tests-config.php ]; then download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php # remove all forward slashes in the end - WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") + WP_CORE_DIR=$(realpath $(echo $WP_CORE_DIR | sed "s:/\+$::")) sed $ioption -E "s:(__DIR__ . '/src/'|dirname\( __FILE__ \) . '/src/'):'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php From f6e325bfab6221ca8b317249d51c8cc520bf56b7 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Mon, 13 Feb 2023 05:42:26 -0300 Subject: [PATCH 163/343] Refactor createOrderedChildren (#36707) * Refactor createOrderedChildren * Add changelog * Add getChildrenAndProps method * Change method comments * Change comment * Change comment `injectProps` * Add comment to explain check --------- Co-authored-by: Fernando Marichal --- ...fix-36614_refactor_create_ordered_children | 4 ++ packages/js/components/src/utils.tsx | 64 +++++++++++++++---- 2 files changed, 56 insertions(+), 12 deletions(-) create mode 100644 packages/js/components/changelog/fix-36614_refactor_create_ordered_children diff --git a/packages/js/components/changelog/fix-36614_refactor_create_ordered_children b/packages/js/components/changelog/fix-36614_refactor_create_ordered_children new file mode 100644 index 00000000000..a347e3adc16 --- /dev/null +++ b/packages/js/components/changelog/fix-36614_refactor_create_ordered_children @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Refactor createOrderedChildren diff --git a/packages/js/components/src/utils.tsx b/packages/js/components/src/utils.tsx index 417d9c169ef..287246ca525 100644 --- a/packages/js/components/src/utils.tsx +++ b/packages/js/components/src/utils.tsx @@ -5,12 +5,58 @@ import { isValidElement, Fragment } from 'react'; import { Slot, Fill } from '@wordpress/components'; import { cloneElement, createElement } from '@wordpress/element'; +type ChildrenProps = { + order: number; +}; + +/** + * Returns an object with the children and props that will be used by `cloneElement`. They will change depending on the + * type of children passed in. + * + * @param {Node} children - Node children. + * @param {number} order - Node order. + * @param {Array} props - Fill props. + * @param {Object} injectProps - Props to inject. + * @return {Object} Object with the keys: children and props. + */ +function getChildrenAndProps< T = Fill.Props, S = Record< string, unknown > >( + children: React.ReactNode, + order: number, + props: T, + injectProps?: S +) { + if ( typeof children === 'function' ) { + return { + children: children( { ...props, order, ...injectProps } ), + props: { order, ...injectProps }, + }; + } else if ( isValidElement( children ) ) { + // This checks whether 'children' is a react element or a standard HTML element. + if ( typeof children?.type === 'function' ) { + return { + children, + props: { + ...props, + order, + ...injectProps, + }, + }; + } + return { + children: children as React.ReactElement< ChildrenProps >, + props: { order }, + }; + } + throw Error( 'Invalid children type' ); +} + /** * Ordered fill item. * - * @param {Node} children - Node children. - * @param {number} order - Node order. - * @param {Array} props - Fill props. + * @param {Node} children - Node children. + * @param {number} order - Node order. + * @param {Array} props - Fill props. + * @param {Object} injectProps - Props to inject. * @return {Node} Node. */ function createOrderedChildren< T = Fill.Props, S = Record< string, unknown > >( @@ -19,15 +65,9 @@ function createOrderedChildren< T = Fill.Props, S = Record< string, unknown > >( props: T, injectProps?: S ) { - if ( typeof children === 'function' ) { - return cloneElement( children( { ...props, order, ...injectProps } ), { - order, - ...injectProps, - } ); - } else if ( isValidElement( children ) ) { - return cloneElement( children, { ...props, order, ...injectProps } ); - } - throw Error( 'Invalid children type' ); + const { children: childrenToRender, props: propsToRender } = + getChildrenAndProps( children, order, props, injectProps ); + return cloneElement( childrenToRender, propsToRender ); } export { createOrderedChildren }; From 3681b84a4c524691ad0748d29992a2d75df87fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20L=C3=B3pez=20Ariza?= <45979455+alopezari@users.noreply.github.com> Date: Mon, 13 Feb 2023 11:34:22 +0100 Subject: [PATCH 164/343] Add checkbox to the pull request template to ask the PR author to read the testing instructions guide before writing them. (#36577) --- .github/PULL_REQUEST_TEMPLATE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7114392b602..2ad59418aac 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -22,7 +22,9 @@ Closes # . ### How to test the changes in this Pull Request: - + + +- [ ] Have you followed the [Writing high-quality testing instructions guide](https://github.com/woocommerce/woocommerce/wiki/Writing-high-quality-testing-instructions)? 1. 2. From 79fa0b9413fc9c468b9f71b992359ab9939b8d11 Mon Sep 17 00:00:00 2001 From: Sam Seay Date: Tue, 14 Feb 2023 00:27:50 +1300 Subject: [PATCH 165/343] Fix permissions for NPM package prepare script (#36732) Contents write is needed to write to pull requests. --- .github/workflows/prepare-package-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prepare-package-release.yml b/.github/workflows/prepare-package-release.yml index 0f311150cf1..3eb728e2130 100644 --- a/.github/workflows/prepare-package-release.yml +++ b/.github/workflows/prepare-package-release.yml @@ -14,8 +14,8 @@ jobs: name: Run prepare script runs-on: ubuntu-20.04 permissions: - contents: read - pull-requests: write + contents: write + pull-requests: write steps: - uses: actions/checkout@v3 From fb3183d2c42bd9b701434ae08f2178af4e3d5fe4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Feb 2023 04:12:09 +1300 Subject: [PATCH 166/343] Prepare Packages for Release (#36814) Automated change: Prep @woocommerce/create-woo-extension for release. Co-authored-by: psealock --- packages/js/create-woo-extension/CHANGELOG.md | 6 ++++++ .../create-woo-extension/changelog/dev-woo-ext-components | 4 ---- packages/js/create-woo-extension/changelog/php-cleanup | 3 --- .../js/create-woo-extension/changelog/update-changelogger | 5 ----- .../changelog/update-create-woo-extension-readme | 4 ---- .../changelog/update-create-woo-extension-woo | 4 ---- packages/js/create-woo-extension/package.json | 2 +- pnpm-lock.yaml | 2 +- 8 files changed, 8 insertions(+), 22 deletions(-) delete mode 100644 packages/js/create-woo-extension/changelog/dev-woo-ext-components delete mode 100644 packages/js/create-woo-extension/changelog/php-cleanup delete mode 100644 packages/js/create-woo-extension/changelog/update-changelogger delete mode 100644 packages/js/create-woo-extension/changelog/update-create-woo-extension-readme delete mode 100644 packages/js/create-woo-extension/changelog/update-create-woo-extension-woo diff --git a/packages/js/create-woo-extension/CHANGELOG.md b/packages/js/create-woo-extension/CHANGELOG.md index 6fefe003856..76f9af7b140 100644 --- a/packages/js/create-woo-extension/CHANGELOG.md +++ b/packages/js/create-woo-extension/CHANGELOG.md @@ -2,6 +2,12 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.2](https://www.npmjs.com/package/@woocommerce/create-woo-extension/v/1.0.2) - 2023-02-13 + +- Patch - Add examples of Woo components package [#36732] +- Patch - bump WooCommerce version [#36732] +- Patch - Update readme [#36732] + ## [1.0.1](https://www.npmjs.com/package/@woocommerce/create-woo-extension/v/1.0.1) - 2022-12-20 - Patch - Fix install scripts [#34385] diff --git a/packages/js/create-woo-extension/changelog/dev-woo-ext-components b/packages/js/create-woo-extension/changelog/dev-woo-ext-components deleted file mode 100644 index acf55a6444d..00000000000 --- a/packages/js/create-woo-extension/changelog/dev-woo-ext-components +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: add - -Add examples of Woo components package diff --git a/packages/js/create-woo-extension/changelog/php-cleanup b/packages/js/create-woo-extension/changelog/php-cleanup deleted file mode 100644 index b9f125755e2..00000000000 --- a/packages/js/create-woo-extension/changelog/php-cleanup +++ /dev/null @@ -1,3 +0,0 @@ -Significance: patch -Type: dev -Comment: Just some PHP clean up to adhere to coding standards diff --git a/packages/js/create-woo-extension/changelog/update-changelogger b/packages/js/create-woo-extension/changelog/update-changelogger deleted file mode 100644 index 1674c919e78..00000000000 --- a/packages/js/create-woo-extension/changelog/update-changelogger +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: fix -Comment: Dev dependency update. - - diff --git a/packages/js/create-woo-extension/changelog/update-create-woo-extension-readme b/packages/js/create-woo-extension/changelog/update-create-woo-extension-readme deleted file mode 100644 index 17691a4a7f8..00000000000 --- a/packages/js/create-woo-extension/changelog/update-create-woo-extension-readme +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Update readme diff --git a/packages/js/create-woo-extension/changelog/update-create-woo-extension-woo b/packages/js/create-woo-extension/changelog/update-create-woo-extension-woo deleted file mode 100644 index 7ddf87a675c..00000000000 --- a/packages/js/create-woo-extension/changelog/update-create-woo-extension-woo +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: update - -bump WooCommerce version diff --git a/packages/js/create-woo-extension/package.json b/packages/js/create-woo-extension/package.json index efb49a732d8..11346857291 100644 --- a/packages/js/create-woo-extension/package.json +++ b/packages/js/create-woo-extension/package.json @@ -1,6 +1,6 @@ { "name": "@woocommerce/create-woo-extension", - "version": "1.0.1", + "version": "1.0.2", "description": "A template to be used with `@wordpress/create-block` to create a WooCommerce extension.", "main": "index.js", "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 329e506d368..618fcabcc47 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38044,7 +38044,7 @@ packages: serialize-javascript: 6.0.0 source-map: 0.6.1 terser: 5.10.0_acorn@8.8.1 - webpack: 5.70.0_webpack-cli@3.3.12 + webpack: 5.70.0 transitivePeerDependencies: - acorn From 2061778b3fb658032910d4f2d5af3b7f9eefd72c Mon Sep 17 00:00:00 2001 From: Nestor Soriano Date: Mon, 13 Feb 2023 17:22:48 +0100 Subject: [PATCH 167/343] Add an encoding selector to the product importer --- .../legacy/js/admin/wc-product-import.js | 32 +++++++------ .../admin/class-wc-admin-importers.php | 27 +++++++---- ...ass-wc-product-csv-importer-controller.php | 47 ++++++++++++------- .../views/html-csv-import-mapping.php | 3 ++ .../views/html-product-csv-import-form.php | 14 ++++++ .../import/class-wc-product-csv-importer.php | 25 +++++++++- 6 files changed, 105 insertions(+), 43 deletions(-) diff --git a/plugins/woocommerce/client/legacy/js/admin/wc-product-import.js b/plugins/woocommerce/client/legacy/js/admin/wc-product-import.js index 645f11b5912..3854dc6af3b 100644 --- a/plugins/woocommerce/client/legacy/js/admin/wc-product-import.js +++ b/plugins/woocommerce/client/legacy/js/admin/wc-product-import.js @@ -5,14 +5,15 @@ * productImportForm handles the import process. */ var productImportForm = function( $form ) { - this.$form = $form; - this.xhr = false; - this.mapping = wc_product_import_params.mapping; - this.position = 0; - this.file = wc_product_import_params.file; - this.update_existing = wc_product_import_params.update_existing; - this.delimiter = wc_product_import_params.delimiter; - this.security = wc_product_import_params.import_nonce; + this.$form = $form; + this.xhr = false; + this.mapping = wc_product_import_params.mapping; + this.position = 0; + this.file = wc_product_import_params.file; + this.update_existing = wc_product_import_params.update_existing; + this.delimiter = wc_product_import_params.delimiter; + this.security = wc_product_import_params.import_nonce; + this.character_encoding = wc_product_import_params.character_encoding; // Number of import successes/failures. this.imported = 0; @@ -39,13 +40,14 @@ type: 'POST', url: ajaxurl, data: { - action : 'woocommerce_do_ajax_product_import', - position : $this.position, - mapping : $this.mapping, - file : $this.file, - update_existing : $this.update_existing, - delimiter : $this.delimiter, - security : $this.security + action : 'woocommerce_do_ajax_product_import', + position : $this.position, + mapping : $this.mapping, + file : $this.file, + update_existing : $this.update_existing, + delimiter : $this.delimiter, + security : $this.security, + character_encoding: $this.character_encoding }, dataType: 'json', success: function( response ) { diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-importers.php b/plugins/woocommerce/includes/admin/class-wc-admin-importers.php index 95f9e3bbca7..11007d8c2c6 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-importers.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-importers.php @@ -147,11 +147,13 @@ class WC_Admin_Importers { public function post_importer_compatibility() { global $wpdb; - if ( empty( $_POST['import_id'] ) || ! class_exists( 'WXR_Parser' ) ) { // PHPCS: input var ok, CSRF ok. + // phpcs:ignore WordPress.Security.NonceVerification.Missing + if ( empty( $_POST['import_id'] ) || ! class_exists( 'WXR_Parser' ) ) { return; } - $id = absint( $_POST['import_id'] ); // PHPCS: input var ok. + // phpcs:ignore WordPress.Security.NonceVerification.Missing + $id = absint( $_POST['import_id'] ); $file = get_attached_file( $id ); $parser = new WXR_Parser(); $import_data = $parser->parse( $file ); @@ -216,12 +218,21 @@ class WC_Admin_Importers { $file = wc_clean( wp_unslash( $_POST['file'] ) ); // PHPCS: input var ok. $params = array( - 'delimiter' => ! empty( $_POST['delimiter'] ) ? wc_clean( wp_unslash( $_POST['delimiter'] ) ) : ',', // PHPCS: input var ok. - 'start_pos' => isset( $_POST['position'] ) ? absint( $_POST['position'] ) : 0, // PHPCS: input var ok. - 'mapping' => isset( $_POST['mapping'] ) ? (array) wc_clean( wp_unslash( $_POST['mapping'] ) ) : array(), // PHPCS: input var ok. - 'update_existing' => isset( $_POST['update_existing'] ) ? (bool) $_POST['update_existing'] : false, // PHPCS: input var ok. - 'lines' => apply_filters( 'woocommerce_product_import_batch_size', 30 ), - 'parse' => true, + 'delimiter' => ! empty( $_POST['delimiter'] ) ? wc_clean( wp_unslash( $_POST['delimiter'] ) ) : ',', // PHPCS: input var ok. + 'start_pos' => isset( $_POST['position'] ) ? absint( $_POST['position'] ) : 0, // PHPCS: input var ok. + 'mapping' => isset( $_POST['mapping'] ) ? (array) wc_clean( wp_unslash( $_POST['mapping'] ) ) : array(), // PHPCS: input var ok. + 'update_existing' => isset( $_POST['update_existing'] ) ? (bool) $_POST['update_existing'] : false, // PHPCS: input var ok. + 'character_encoding' => isset( $_POST['character_encoding'] ) ? wc_clean( wp_unslash( $_POST['character_encoding'] ) ) : '', + + /** + * Batch size for the product import process. + * + * @param int $size Batch size. + * + * @since + */ + 'lines' => apply_filters( 'woocommerce_product_import_batch_size', 30 ), + 'parse' => true, ); // Log failures. diff --git a/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php index a2e69186818..df11ab23963 100644 --- a/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php +++ b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php @@ -70,6 +70,13 @@ class WC_Product_CSV_Importer_Controller { */ protected $update_existing = false; + /** + * The character encoding to use to interpret the input file, or empty string for autodetect. + * + * @var string + */ + protected $character_encoding = 'UTF-8'; + /** * Get importer instance. * @@ -139,11 +146,12 @@ class WC_Product_CSV_Importer_Controller { $this->steps = apply_filters( 'woocommerce_product_csv_importer_steps', $default_steps ); // phpcs:disable WordPress.Security.NonceVerification.Recommended - $this->step = isset( $_REQUEST['step'] ) ? sanitize_key( $_REQUEST['step'] ) : current( array_keys( $this->steps ) ); - $this->file = isset( $_REQUEST['file'] ) ? wc_clean( wp_unslash( $_REQUEST['file'] ) ) : ''; - $this->update_existing = isset( $_REQUEST['update_existing'] ) ? (bool) $_REQUEST['update_existing'] : false; - $this->delimiter = ! empty( $_REQUEST['delimiter'] ) ? wc_clean( wp_unslash( $_REQUEST['delimiter'] ) ) : ','; - $this->map_preferences = isset( $_REQUEST['map_preferences'] ) ? (bool) $_REQUEST['map_preferences'] : false; + $this->step = isset( $_REQUEST['step'] ) ? sanitize_key( $_REQUEST['step'] ) : current( array_keys( $this->steps ) ); + $this->file = isset( $_REQUEST['file'] ) ? wc_clean( wp_unslash( $_REQUEST['file'] ) ) : ''; + $this->update_existing = isset( $_REQUEST['update_existing'] ) ? (bool) $_REQUEST['update_existing'] : false; + $this->delimiter = ! empty( $_REQUEST['delimiter'] ) ? wc_clean( wp_unslash( $_REQUEST['delimiter'] ) ) : ','; + $this->map_preferences = isset( $_REQUEST['map_preferences'] ) ? (bool) $_REQUEST['map_preferences'] : false; + $this->character_encoding = isset( $_REQUEST['character_encoding'] ) ? wc_clean( wp_unslash( $_REQUEST['character_encoding'] ) ) : 'UTF-8'; // phpcs:enable // Import mappings for CSV data. @@ -180,12 +188,13 @@ class WC_Product_CSV_Importer_Controller { } $params = array( - 'step' => $keys[ $step_index + 1 ], - 'file' => str_replace( DIRECTORY_SEPARATOR, '/', $this->file ), - 'delimiter' => $this->delimiter, - 'update_existing' => $this->update_existing, - 'map_preferences' => $this->map_preferences, - '_wpnonce' => wp_create_nonce( 'woocommerce-csv-importer' ), // wp_nonce_url() escapes & to & breaking redirects. + 'step' => $keys[ $step_index + 1 ], + 'file' => str_replace( DIRECTORY_SEPARATOR, '/', $this->file ), + 'delimiter' => $this->delimiter, + 'update_existing' => $this->update_existing, + 'map_preferences' => $this->map_preferences, + 'character_encoding' => $this->character_encoding, + '_wpnonce' => wp_create_nonce( 'woocommerce-csv-importer' ), // wp_nonce_url() escapes & to & breaking redirects. ); return add_query_arg( $params ); @@ -365,8 +374,9 @@ class WC_Product_CSV_Importer_Controller { protected function mapping_form() { check_admin_referer( 'woocommerce-csv-importer' ); $args = array( - 'lines' => 1, - 'delimiter' => $this->delimiter, + 'lines' => 1, + 'delimiter' => $this->delimiter, + 'character_encoding' => $this->character_encoding, ); $importer = self::get_importer( $this->file, $args ); @@ -428,14 +438,15 @@ class WC_Product_CSV_Importer_Controller { 'wc-product-import', 'wc_product_import_params', array( - 'import_nonce' => wp_create_nonce( 'wc-product-import' ), - 'mapping' => array( + 'import_nonce' => wp_create_nonce( 'wc-product-import' ), + 'mapping' => array( 'from' => $mapping_from, 'to' => $mapping_to, ), - 'file' => $this->file, - 'update_existing' => $this->update_existing, - 'delimiter' => $this->delimiter, + 'file' => $this->file, + 'update_existing' => $this->update_existing, + 'delimiter' => $this->delimiter, + 'character_encoding' => $this->character_encoding, ) ); wp_enqueue_script( 'wc-product-import' ); diff --git a/plugins/woocommerce/includes/admin/importers/views/html-csv-import-mapping.php b/plugins/woocommerce/includes/admin/importers/views/html-csv-import-mapping.php index 20cdc422c7c..38a74443f3f 100644 --- a/plugins/woocommerce/includes/admin/importers/views/html-csv-import-mapping.php +++ b/plugins/woocommerce/includes/admin/importers/views/html-csv-import-mapping.php @@ -60,6 +60,9 @@ if ( ! defined( 'ABSPATH' ) ) { + + + diff --git a/plugins/woocommerce/includes/admin/importers/views/html-product-csv-import-form.php b/plugins/woocommerce/includes/admin/importers/views/html-product-csv-import-form.php index 3b96ec64f47..94f9979ff13 100644 --- a/plugins/woocommerce/includes/admin/importers/views/html-product-csv-import-form.php +++ b/plugins/woocommerce/includes/admin/importers/views/html-product-csv-import-form.php @@ -78,6 +78,20 @@ if ( ! defined( 'ABSPATH' ) ) {

+ + + +

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 c5f828f7d3a..6a32aaaf9c9 100644 --- a/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php +++ b/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php @@ -66,6 +66,17 @@ class WC_Product_CSV_Importer extends WC_Product_Importer { $this->read_file(); } + /** + * Convert a string from the input encoding to UTF-8. + * + * @param string $value The string to convert. + * @return string The converted string. + */ + private function adjust_character_encoding( $value ) { + $encoding = $this->params['character_encoding']; + return 'UTF-8' === $encoding ? $value : mb_convert_encoding( $value, 'UTF-8', $encoding ); + } + /** * Read file. */ @@ -77,7 +88,11 @@ class WC_Product_CSV_Importer extends WC_Product_Importer { $handle = fopen( $this->file, 'r' ); // @codingStandardsIgnoreLine. 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 + $this->raw_keys = array_map( 'trim', fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ) ); // @codingStandardsIgnoreLine + + if ( $this->params['character_encoding'] ) { + $this->raw_keys = array_map( array( $this, 'adjust_character_encoding' ), $this->raw_keys ); + } // Remove line breaks in keys, to avoid mismatch mapping of keys. $this->raw_keys = wc_clean( wp_unslash( $this->raw_keys ) ); @@ -92,9 +107,13 @@ class WC_Product_CSV_Importer extends WC_Product_Importer { } while ( 1 ) { - $row = version_compare( PHP_VERSION, '5.3', '>=' ) ? fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ) : fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'] ); // @codingStandardsIgnoreLine + $row = fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ); // @codingStandardsIgnoreLine if ( false !== $row ) { + if ( $this->params['character_encoding'] ) { + $row = array_map( array( $this, 'adjust_character_encoding' ), $row ); + } + $this->raw_data[] = $row; $this->file_positions[ count( $this->raw_data ) ] = ftell( $handle ); @@ -1005,6 +1024,8 @@ class WC_Product_CSV_Importer extends WC_Product_Importer { * * @param array $parsed_data Parsed data. * @param WC_Product_Importer $importer Importer instance. + * + * @since */ $this->parsed_data[] = apply_filters( 'woocommerce_product_importer_parsed_data', $this->expand_data( $data ), $this ); } From 13a130fe37d34a8609bce8637a6284aaf329d6d5 Mon Sep 17 00:00:00 2001 From: Nestor Soriano Date: Mon, 13 Feb 2023 17:24:08 +0100 Subject: [PATCH 168/343] Add changelog file --- .../changelog/add-encoding-selector-to-product-importer | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-encoding-selector-to-product-importer diff --git a/plugins/woocommerce/changelog/add-encoding-selector-to-product-importer b/plugins/woocommerce/changelog/add-encoding-selector-to-product-importer new file mode 100644 index 00000000000..7d2631b3da9 --- /dev/null +++ b/plugins/woocommerce/changelog/add-encoding-selector-to-product-importer @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add an encoding selector to the product importer From 98f325c776b42c37e8fa62928b17a6ebf77e63cf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Feb 2023 14:16:22 -0600 Subject: [PATCH 169/343] Delete changelog files based on PR 36456 (#36582) Delete changelog files for 36456 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/disable-new-navigation | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/disable-new-navigation diff --git a/plugins/woocommerce/changelog/disable-new-navigation b/plugins/woocommerce/changelog/disable-new-navigation deleted file mode 100644 index 3a246245bd1..00000000000 --- a/plugins/woocommerce/changelog/disable-new-navigation +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: update - -Disable the new navigation as the feature is on hold. From 55fc851dba9cbb9b89e7163aeee23b998077ed00 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Feb 2023 14:16:43 -0600 Subject: [PATCH 170/343] Delete changelog files based on PR 36714 (#36717) Delete changelog files for 36714 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/revert-36294 | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 plugins/woocommerce/changelog/revert-36294 diff --git a/plugins/woocommerce/changelog/revert-36294 b/plugins/woocommerce/changelog/revert-36294 deleted file mode 100644 index ac985b65a44..00000000000 --- a/plugins/woocommerce/changelog/revert-36294 +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: This is a revert: we don't need a further changelog entry and the existing entry should be removed by the revert itself. - - From 19afaccb9da103a7bdcacc8c74eaf579aac1a153 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Feb 2023 14:17:00 -0600 Subject: [PATCH 171/343] Delete changelog files based on PR 36736 (#36744) Delete changelog files for 36736 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/update-woocommerce-blocks-9.4.3 | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/update-woocommerce-blocks-9.4.3 diff --git a/plugins/woocommerce/changelog/update-woocommerce-blocks-9.4.3 b/plugins/woocommerce/changelog/update-woocommerce-blocks-9.4.3 deleted file mode 100644 index 6da8a6cd913..00000000000 --- a/plugins/woocommerce/changelog/update-woocommerce-blocks-9.4.3 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: update - -Update woocommerce-blocks to 9.4.3. From 2da44aea66efa20907f9113a94f895521148d46b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Feb 2023 14:18:36 -0600 Subject: [PATCH 172/343] Delete changelog files based on PR 36624 (#36631) Delete changelog files for 36624 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/update-woocommerce-blocks-9.4.2 | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/update-woocommerce-blocks-9.4.2 diff --git a/plugins/woocommerce/changelog/update-woocommerce-blocks-9.4.2 b/plugins/woocommerce/changelog/update-woocommerce-blocks-9.4.2 deleted file mode 100644 index e055f92989d..00000000000 --- a/plugins/woocommerce/changelog/update-woocommerce-blocks-9.4.2 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: update - -Update WooCommerce Blocks to 9.4.2 From 906c84b85e8ca9dfe1db1bb8ae7afac6405de64c Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Tue, 14 Feb 2023 00:45:54 +0100 Subject: [PATCH 173/343] Make the "state" checkout field optional for BG and HU (#36701) * Make state optional for BG and HU. * Add changelog. * PHPCS spacing fix * Hide State field for Hungary. * Unhide the State in Hungary. Some extensions might still use it e.g. for shipping purposes. * Merge branch 'trunk' into fix/hu-and-bg-state-optional --- .../woocommerce/changelog/fix-hu-and-bg-state-optional | 4 ++++ plugins/woocommerce/includes/class-wc-countries.php | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-hu-and-bg-state-optional diff --git a/plugins/woocommerce/changelog/fix-hu-and-bg-state-optional b/plugins/woocommerce/changelog/fix-hu-and-bg-state-optional new file mode 100644 index 00000000000..a9b371a7582 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-hu-and-bg-state-optional @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Make states optional for Hungary and Bulgaria. diff --git a/plugins/woocommerce/includes/class-wc-countries.php b/plugins/woocommerce/includes/class-wc-countries.php index c88e63b55c1..f4fa5ceab6e 100644 --- a/plugins/woocommerce/includes/class-wc-countries.php +++ b/plugins/woocommerce/includes/class-wc-countries.php @@ -882,6 +882,11 @@ class WC_Countries { 'hidden' => true, ), ), + 'BG' => array( + 'state' => array( + 'required' => false, + ), + ), 'BH' => array( 'postcode' => array( 'required' => false, @@ -1115,7 +1120,8 @@ class WC_Countries { 'priority' => 72, ), 'state' => array( - 'label' => __( 'County', 'woocommerce' ), + 'label' => __( 'County', 'woocommerce' ), + 'required' => false, ), ), 'ID' => array( From a3e26fe9eaff1dff0ed97db72352b45fd86aade2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Feb 2023 09:43:17 +0800 Subject: [PATCH 174/343] Prepare Packages for Release (#36825) Automated change: Prep @woocommerce/onboarding for release. Co-authored-by: chihsuan --- packages/js/onboarding/CHANGELOG.md | 16 ++++++++++++++-- .../changelog/add-36465-tasklist-extensibility | 4 ---- .../changelog/dev-bump-pnpm-version-restraint | 4 ---- .../dev-clean-up-product-layout-exp-codes | 4 ---- .../changelog/dev-consolidate-eslint-versions | 4 ---- .../changelog/dev-fix-pnpm-version-engines | 4 ---- .../js/onboarding/changelog/dev-simplify-turbo | 5 ----- .../changelog/fix-35704-wcpay-benefit-padding | 4 ---- .../fix-missing-type-definitions-dependencies | 4 ---- .../onboarding/changelog/fix-plugin-installer-ts | 4 ---- .../js/onboarding/changelog/update-changelogger | 5 ----- packages/js/onboarding/changelog/upgrade-pnpm-7 | 4 ---- packages/js/onboarding/package.json | 2 +- 13 files changed, 15 insertions(+), 49 deletions(-) delete mode 100644 packages/js/onboarding/changelog/add-36465-tasklist-extensibility delete mode 100644 packages/js/onboarding/changelog/dev-bump-pnpm-version-restraint delete mode 100644 packages/js/onboarding/changelog/dev-clean-up-product-layout-exp-codes delete mode 100644 packages/js/onboarding/changelog/dev-consolidate-eslint-versions delete mode 100644 packages/js/onboarding/changelog/dev-fix-pnpm-version-engines delete mode 100644 packages/js/onboarding/changelog/dev-simplify-turbo delete mode 100644 packages/js/onboarding/changelog/fix-35704-wcpay-benefit-padding delete mode 100644 packages/js/onboarding/changelog/fix-missing-type-definitions-dependencies delete mode 100644 packages/js/onboarding/changelog/fix-plugin-installer-ts delete mode 100644 packages/js/onboarding/changelog/update-changelogger delete mode 100644 packages/js/onboarding/changelog/upgrade-pnpm-7 diff --git a/packages/js/onboarding/CHANGELOG.md b/packages/js/onboarding/CHANGELOG.md index fe7f04ba1c5..462aad61575 100644 --- a/packages/js/onboarding/CHANGELOG.md +++ b/packages/js/onboarding/CHANGELOG.md @@ -2,12 +2,24 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [3.2.0](https://www.npmjs.com/package/@woocommerce/packages/js/onboarding/v/3.2.0) - 2022-07-08 +## [3.3.0](https://www.npmjs.com/package/@woocommerce/onboarding/v/3.3.0) - 2023-02-14 + +- Patch - Added in missing TS definitions in package.json [#36701] +- Patch - Fix wcpay benefits padding [#36701] +- Minor - Add WooOnboardingTaskListHeader component [#36701] +- Minor - Adjust build/test scripts to remove -- -- that was required for pnpm 6. [#36701] +- Patch - Cleanup product task experiment [#36701] +- Minor - Fix node and pnpm versions via engines [#36701] +- Minor - Match TypeScript version with syncpack [#36701] +- Patch - Update eslint to 8.32.0 across the monorepo. [#36701] +- Minor - Update pnpm version constraint to 7.13.3 to avoid auto-install-peers issues [#36701] + +## [3.2.0](https://www.npmjs.com/package/@woocommerce/onboarding/v/3.2.0) - 2022-07-08 - Minor - Add WCPayBanner & WCPayBenefits components - Minor - Remove PHP and Composer dependencies for packaged JS packages -## [3.1.0](https://www.npmjs.com/package/@woocommerce/packages/js/onboarding/v/3.1.0) - 2022-06-15 +## [3.1.0](https://www.npmjs.com/package/@woocommerce/onboarding/v/3.1.0) - 2022-06-15 - Minor - Add ExPlat dependency and product task experiment logic - Minor - Add Jetpack Changelogger diff --git a/packages/js/onboarding/changelog/add-36465-tasklist-extensibility b/packages/js/onboarding/changelog/add-36465-tasklist-extensibility deleted file mode 100644 index 18e3bc6d5ae..00000000000 --- a/packages/js/onboarding/changelog/add-36465-tasklist-extensibility +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add WooOnboardingTaskListHeader component diff --git a/packages/js/onboarding/changelog/dev-bump-pnpm-version-restraint b/packages/js/onboarding/changelog/dev-bump-pnpm-version-restraint deleted file mode 100644 index f7511cb6974..00000000000 --- a/packages/js/onboarding/changelog/dev-bump-pnpm-version-restraint +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Update pnpm version constraint to 7.13.3 to avoid auto-install-peers issues diff --git a/packages/js/onboarding/changelog/dev-clean-up-product-layout-exp-codes b/packages/js/onboarding/changelog/dev-clean-up-product-layout-exp-codes deleted file mode 100644 index 4229640cc51..00000000000 --- a/packages/js/onboarding/changelog/dev-clean-up-product-layout-exp-codes +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Cleanup product task experiment diff --git a/packages/js/onboarding/changelog/dev-consolidate-eslint-versions b/packages/js/onboarding/changelog/dev-consolidate-eslint-versions deleted file mode 100644 index d3d95c39119..00000000000 --- a/packages/js/onboarding/changelog/dev-consolidate-eslint-versions +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/onboarding/changelog/dev-fix-pnpm-version-engines b/packages/js/onboarding/changelog/dev-fix-pnpm-version-engines deleted file mode 100644 index a1804a282f0..00000000000 --- a/packages/js/onboarding/changelog/dev-fix-pnpm-version-engines +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Fix node and pnpm versions via engines diff --git a/packages/js/onboarding/changelog/dev-simplify-turbo b/packages/js/onboarding/changelog/dev-simplify-turbo deleted file mode 100644 index 0d230384010..00000000000 --- a/packages/js/onboarding/changelog/dev-simplify-turbo +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: Package scripts were modified to support simplified running of turbo commands in the monorepo. - - diff --git a/packages/js/onboarding/changelog/fix-35704-wcpay-benefit-padding b/packages/js/onboarding/changelog/fix-35704-wcpay-benefit-padding deleted file mode 100644 index 77755055d34..00000000000 --- a/packages/js/onboarding/changelog/fix-35704-wcpay-benefit-padding +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix wcpay benefits padding diff --git a/packages/js/onboarding/changelog/fix-missing-type-definitions-dependencies b/packages/js/onboarding/changelog/fix-missing-type-definitions-dependencies deleted file mode 100644 index 06ebf8aa257..00000000000 --- a/packages/js/onboarding/changelog/fix-missing-type-definitions-dependencies +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Added in missing TS definitions in package.json \ No newline at end of file diff --git a/packages/js/onboarding/changelog/fix-plugin-installer-ts b/packages/js/onboarding/changelog/fix-plugin-installer-ts deleted file mode 100644 index 77d0a173670..00000000000 --- a/packages/js/onboarding/changelog/fix-plugin-installer-ts +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Match TypeScript version with syncpack diff --git a/packages/js/onboarding/changelog/update-changelogger b/packages/js/onboarding/changelog/update-changelogger deleted file mode 100644 index 1674c919e78..00000000000 --- a/packages/js/onboarding/changelog/update-changelogger +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: fix -Comment: Dev dependency update. - - diff --git a/packages/js/onboarding/changelog/upgrade-pnpm-7 b/packages/js/onboarding/changelog/upgrade-pnpm-7 deleted file mode 100644 index 10ee28d636f..00000000000 --- a/packages/js/onboarding/changelog/upgrade-pnpm-7 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Adjust build/test scripts to remove -- -- that was required for pnpm 6. diff --git a/packages/js/onboarding/package.json b/packages/js/onboarding/package.json index 96dd68e72dc..058a207c105 100644 --- a/packages/js/onboarding/package.json +++ b/packages/js/onboarding/package.json @@ -1,6 +1,6 @@ { "name": "@woocommerce/onboarding", - "version": "3.2.0", + "version": "3.3.0", "description": "Onboarding utilities.", "author": "Automattic", "license": "GPL-3.0-or-later", From c5cbc8d4ab8f353fbc8d67a41598390d8e8763aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomek=20Wytr=C4=99bowicz?= Date: Tue, 14 Feb 2023 06:34:00 +0100 Subject: [PATCH 175/343] Update `config@3.3.7` (from `3.3.3`) (#33828) * Update `config@3.3.7` (from `3.3.3`) Add it to syncpack, to avoid `ReferenceError: node_env_var_name is not defined` when external extension imports and transforms the `admin-e2e-tests` directly from `node_modules`. Include https://github.com/node-config/node-config/pull/642 --- .syncpackrc | 11 +- .../changelog/update-bump-config | 4 + packages/js/admin-e2e-tests/package.json | 2 +- packages/js/e2e-core-tests/package.json | 2 +- packages/js/e2e-environment/package.json | 2 +- packages/js/e2e-utils/package.json | 2 +- plugins/woocommerce-admin/package.json | 2 +- .../changelog/update-bump-config-2 | 4 + plugins/woocommerce/package.json | 2 +- pnpm-lock.yaml | 2542 ++++++++--------- 10 files changed, 1241 insertions(+), 1332 deletions(-) create mode 100644 packages/js/admin-e2e-tests/changelog/update-bump-config create mode 100644 plugins/woocommerce/changelog/update-bump-config-2 diff --git a/.syncpackrc b/.syncpackrc index b2031900baf..8f228343559 100644 --- a/.syncpackrc +++ b/.syncpackrc @@ -1,6 +1,6 @@ { "dev": true, - "filter": "^(?:react|react-dom|eslint|typescript|@typescript-eslint|@types/react).*$", + "filter": "^(?:config|react|react-dom|eslint|typescript|@typescript-eslint|@types/react).*$", "indent": "\t", "overrides": true, "peer": true, @@ -33,6 +33,15 @@ "**" ] }, + { + "dependencies": [ + "config" + ], + "packages": [ + "**" + ], + "pinVersion": "3.3.7" + }, { "dependencies": [ "react", diff --git a/packages/js/admin-e2e-tests/changelog/update-bump-config b/packages/js/admin-e2e-tests/changelog/update-bump-config new file mode 100644 index 00000000000..c89120d3892 --- /dev/null +++ b/packages/js/admin-e2e-tests/changelog/update-bump-config @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Unify semver range for `config@3.3.7` (from `^3.3.7`). Fix `node_env_var_name is not defined` error. diff --git a/packages/js/admin-e2e-tests/package.json b/packages/js/admin-e2e-tests/package.json index df3e15d3f0d..51bffa6e593 100644 --- a/packages/js/admin-e2e-tests/package.json +++ b/packages/js/admin-e2e-tests/package.json @@ -29,7 +29,7 @@ "@jest/globals": "^27.5.1", "@types/jest": "^27.4.1", "@woocommerce/e2e-utils": "workspace:*", - "config": "^3.3.7" + "config": "3.3.7" }, "peerDependencies": { "@woocommerce/e2e-environment": "^0.2.3 || ^0.3.0", diff --git a/packages/js/e2e-core-tests/package.json b/packages/js/e2e-core-tests/package.json index 70ef896ba73..17d7e5fae6d 100644 --- a/packages/js/e2e-core-tests/package.json +++ b/packages/js/e2e-core-tests/package.json @@ -25,7 +25,7 @@ "dependencies": { "@jest/globals": "^27.5.1", "@wordpress/deprecated": "^3.2.3", - "config": "3.3.3" + "config": "3.3.7" }, "devDependencies": { "@babel/cli": "7.12.8", diff --git a/packages/js/e2e-environment/package.json b/packages/js/e2e-environment/package.json index 8c7b0fb5f39..ed60925c913 100644 --- a/packages/js/e2e-environment/package.json +++ b/packages/js/e2e-environment/package.json @@ -33,7 +33,7 @@ "@wordpress/jest-preset-default": "^7.1.3", "app-root-path": "^3.0.0", "commander": "4.1.1", - "config": "3.3.3", + "config": "3.3.7", "jest": "^27.5.1", "jest-circus": "27.5.1", "jest-each": "27.5.1", diff --git a/packages/js/e2e-utils/package.json b/packages/js/e2e-utils/package.json index ebc1a883d91..5695a8f3df3 100644 --- a/packages/js/e2e-utils/package.json +++ b/packages/js/e2e-utils/package.json @@ -18,7 +18,7 @@ "@automattic/puppeteer-utils": "github:Automattic/puppeteer-utils#0f3ec50", "@wordpress/deprecated": "^3.2.3", "@wordpress/e2e-test-utils": "wp-5.8", - "config": "3.3.3", + "config": "3.3.7", "fishery": "^1.2.0" }, "devDependencies": { diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index d04562a4d72..42e60d51c29 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -173,7 +173,7 @@ "chalk": "^4.1.2", "comment-parser": "^1.3.0", "concurrently": "^7.0.0", - "config": "^3.3.7", + "config": "3.3.7", "copy-webpack-plugin": "^10.2.4", "cross-env": "^7.0.3", "css-loader": "^6.7.0", diff --git a/plugins/woocommerce/changelog/update-bump-config-2 b/plugins/woocommerce/changelog/update-bump-config-2 new file mode 100644 index 00000000000..a15a38cf433 --- /dev/null +++ b/plugins/woocommerce/changelog/update-bump-config-2 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Update `config@3.3.7` (from `3.3.3`). Fix `node_env_var_name is not defined` error. diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index 83a77b28d1e..1c13d294002 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -73,7 +73,7 @@ "babel-eslint": "10.1.0", "chai": "4.2.0", "chai-as-promised": "7.1.1", - "config": "3.3.3", + "config": "3.3.7", "cross-env": "6.0.3", "deasync": "0.1.26", "dotenv": "^10.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 618fcabcc47..ce606e55040 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,7 +90,7 @@ importers: '@woocommerce/e2e-environment': ^0.2.3 || ^0.3.0 '@woocommerce/e2e-utils': workspace:* '@woocommerce/eslint-plugin': workspace:* - config: ^3.3.7 + config: 3.3.7 eslint: ^8.32.0 jest: ^27.5.1 jest-cli: ^27.5.1 @@ -705,7 +705,7 @@ importers: '@wordpress/babel-preset-default': 3.0.2 '@wordpress/browserslist-config': ^4.1.0 '@wordpress/deprecated': ^3.2.3 - config: 3.3.3 + config: 3.3.7 eslint: ^8.32.0 eslint-plugin-jest: 23.20.0 dependencies: @@ -713,7 +713,7 @@ importers: '@woocommerce/api': link:../api '@woocommerce/e2e-utils': 0.1.6_drwlo4y73cla4rzwrrjk6eyf6e '@wordpress/deprecated': 3.2.3 - config: 3.3.3 + config: 3.3.7 devDependencies: '@babel/cli': 7.12.8_@babel+core@7.12.9 '@babel/core': 7.12.9 @@ -754,7 +754,7 @@ importers: '@wordpress/jest-preset-default': ^7.1.3 app-root-path: ^3.0.0 commander: 4.1.1 - config: 3.3.3 + config: 3.3.7 jest: ^27.5.1 jest-circus: 27.5.1 jest-each: 27.5.1 @@ -775,7 +775,7 @@ importers: '@wordpress/jest-preset-default': 7.1.3_lzj7uau34542hrpvigopp7itta app-root-path: 3.0.0 commander: 4.1.1 - config: 3.3.3 + config: 3.3.7 jest: 27.5.1 jest-circus: 27.5.1 jest-each: 27.5.1 @@ -823,7 +823,7 @@ importers: '@wordpress/browserslist-config': ^4.1.0 '@wordpress/deprecated': ^3.2.3 '@wordpress/e2e-test-utils': wp-5.8 - config: 3.3.3 + config: 3.3.7 eslint: ^8.32.0 eslint-plugin-jest: 23.20.0 fishery: ^1.2.0 @@ -832,7 +832,7 @@ importers: '@woocommerce/api': link:../api '@wordpress/deprecated': 3.2.3 '@wordpress/e2e-test-utils': 5.3.2_ujr7gcpwq6xmoiv7mmimozpxs4 - config: 3.3.3 + config: 3.3.7 fishery: 1.4.0 devDependencies: '@babel/cli': 7.12.8_@babel+core@7.12.9 @@ -1383,7 +1383,7 @@ importers: babel-eslint: 10.1.0 chai: 4.2.0 chai-as-promised: 7.1.1 - config: 3.3.3 + config: 3.3.7 cross-env: 6.0.3 deasync: 0.1.26 dotenv: ^10.0.0 @@ -1430,7 +1430,7 @@ importers: babel-eslint: 10.1.0_eslint@8.32.0 chai: 4.2.0 chai-as-promised: 7.1.1_chai@4.2.0 - config: 3.3.3 + config: 3.3.7 cross-env: 6.0.3 deasync: 0.1.26 dotenv: 10.0.0 @@ -1560,7 +1560,7 @@ importers: classnames: ^2.3.1 comment-parser: ^1.3.0 concurrently: ^7.0.0 - config: ^3.3.7 + config: 3.3.7 copy-webpack-plugin: ^10.2.4 core-js: ^3.21.1 cross-env: ^7.0.3 @@ -2518,6 +2518,7 @@ packages: /@babel/compat-data/7.17.7: resolution: {integrity: sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ==} engines: {node: '>=6.9.0'} + dev: true /@babel/compat-data/7.19.3: resolution: {integrity: sha512-prBHMK4JYYK+wDjJF1q99KK4JLL+egWS4nmNqdlMUgCExMZ+iZW0hGhyC3VEbsPjvaN0TBhW//VIFwBrk8sEiw==} @@ -2660,6 +2661,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.19.3 + dev: true /@babel/helper-annotate-as-pure/7.18.6: resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} @@ -2696,7 +2698,7 @@ packages: '@babel/compat-data': 7.19.3 '@babel/core': 7.17.8 '@babel/helper-validator-option': 7.18.6 - browserslist: 4.21.4 + browserslist: 4.19.3 semver: 6.3.0 dev: true @@ -2738,42 +2740,6 @@ packages: browserslist: 4.21.4 semver: 6.3.0 - /@babel/helper-create-class-features-plugin/7.17.6_@babel+core@7.12.9: - resolution: {integrity: sha512-SogLLSxXm2OkBbSsHZMM4tUi8fUzjs63AT/d0YQIzr6GSd8Hxsbk2KYDX0k0DweAzGMj/YWeiCsorIdtdcW8Eg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-member-expression-to-functions': 7.18.9 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/helper-replace-supers': 7.19.1 - '@babel/helper-split-export-declaration': 7.18.6 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/helper-create-class-features-plugin/7.17.6_@babel+core@7.16.12: - resolution: {integrity: sha512-SogLLSxXm2OkBbSsHZMM4tUi8fUzjs63AT/d0YQIzr6GSd8Hxsbk2KYDX0k0DweAzGMj/YWeiCsorIdtdcW8Eg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-member-expression-to-functions': 7.18.9 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/helper-replace-supers': 7.19.1 - '@babel/helper-split-export-declaration': 7.18.6 - transitivePeerDependencies: - - supports-color - dev: false - /@babel/helper-create-class-features-plugin/7.17.6_@babel+core@7.17.8: resolution: {integrity: sha512-SogLLSxXm2OkBbSsHZMM4tUi8fUzjs63AT/d0YQIzr6GSd8Hxsbk2KYDX0k0DweAzGMj/YWeiCsorIdtdcW8Eg==} engines: {node: '>=6.9.0'} @@ -2981,6 +2947,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.19.3 + dev: true /@babel/helper-module-imports/7.18.6: resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} @@ -3060,6 +3027,7 @@ packages: '@babel/types': 7.19.3 transitivePeerDependencies: - supports-color + dev: true /@babel/helper-remap-async-to-generator/7.18.9_@babel+core@7.12.9: resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==} @@ -3199,26 +3167,6 @@ packages: dependencies: '@babel/types': 7.19.3 - /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-anv/DObl7waiGEnC24O9zqL0pSuI9hljihqiDuFHC8d7/bjr/4RLGPWuc8rYOff/QPzbEPSkzG8wGG9aDuhHRg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-anv/DObl7waiGEnC24O9zqL0pSuI9hljihqiDuFHC8d7/bjr/4RLGPWuc8rYOff/QPzbEPSkzG8wGG9aDuhHRg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-anv/DObl7waiGEnC24O9zqL0pSuI9hljihqiDuFHC8d7/bjr/4RLGPWuc8rYOff/QPzbEPSkzG8wGG9aDuhHRg==} engines: {node: '>=6.9.0'} @@ -3229,6 +3177,26 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} engines: {node: '>=6.9.0'} @@ -3238,30 +3206,6 @@ packages: '@babel/core': 7.17.8 '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-di8vUHRdf+4aJ7ltXhaDbPoszdkh59AQtJM5soLsuHpQJdFQZOA4uGj0V2u/CZ8bJ/u8ULDL5yq6FO/bCXnKHw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.13.0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 - '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.12.9 - dev: true - - /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-di8vUHRdf+4aJ7ltXhaDbPoszdkh59AQtJM5soLsuHpQJdFQZOA4uGj0V2u/CZ8bJ/u8ULDL5yq6FO/bCXnKHw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.13.0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 - '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.16.12 - dev: false - /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-di8vUHRdf+4aJ7ltXhaDbPoszdkh59AQtJM5soLsuHpQJdFQZOA4uGj0V2u/CZ8bJ/u8ULDL5yq6FO/bCXnKHw==} engines: {node: '>=6.9.0'} @@ -3274,6 +3218,30 @@ packages: '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.17.8 dev: true + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/7.18.9_@babel+core@7.12.9: + resolution: {integrity: sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 + '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.12.9 + dev: true + + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/7.18.9_@babel+core@7.16.12: + resolution: {integrity: sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 + '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.16.12 + dev: false + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/7.18.9_@babel+core@7.17.8: resolution: {integrity: sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==} engines: {node: '>=6.9.0'} @@ -3299,34 +3267,6 @@ packages: - supports-color dev: true - /@babel/plugin-proposal-async-generator-functions/7.16.8_@babel+core@7.12.9: - resolution: {integrity: sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.12.9 - '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.12.9 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-proposal-async-generator-functions/7.16.8_@babel+core@7.16.12: - resolution: {integrity: sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.16.12 - '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.16.12 - transitivePeerDependencies: - - supports-color - dev: false - /@babel/plugin-proposal-async-generator-functions/7.16.8_@babel+core@7.17.8: resolution: {integrity: sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ==} engines: {node: '>=6.9.0'} @@ -3341,6 +3281,36 @@ packages: - supports-color dev: true + /@babel/plugin-proposal-async-generator-functions/7.19.1_@babel+core@7.12.9: + resolution: {integrity: sha512-0yu8vNATgLy4ivqMNBIwb1HebCelqN7YX8SL3FDXORv/RqT0zEEWUCH4GH44JsSrvCu6GqnAdR5EBFAPeNBB4Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.12.9 + '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.12.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-async-generator-functions/7.19.1_@babel+core@7.16.12: + resolution: {integrity: sha512-0yu8vNATgLy4ivqMNBIwb1HebCelqN7YX8SL3FDXORv/RqT0zEEWUCH4GH44JsSrvCu6GqnAdR5EBFAPeNBB4Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.16.12 + '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.16.12 + transitivePeerDependencies: + - supports-color + dev: false + /@babel/plugin-proposal-async-generator-functions/7.19.1_@babel+core@7.17.8: resolution: {integrity: sha512-0yu8vNATgLy4ivqMNBIwb1HebCelqN7YX8SL3FDXORv/RqT0zEEWUCH4GH44JsSrvCu6GqnAdR5EBFAPeNBB4Q==} engines: {node: '>=6.9.0'} @@ -3368,32 +3338,6 @@ packages: - supports-color dev: true - /@babel/plugin-proposal-class-properties/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-IobU0Xme31ewjYOShSIqd/ZGM/r/cuOz2z0MDbNrhF5FW+ZVgi0f2lyeoj9KFPDOAqsYxmLWZte1WOwlvY9aww==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-proposal-class-properties/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-IobU0Xme31ewjYOShSIqd/ZGM/r/cuOz2z0MDbNrhF5FW+ZVgi0f2lyeoj9KFPDOAqsYxmLWZte1WOwlvY9aww==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.18.9 - transitivePeerDependencies: - - supports-color - dev: false - /@babel/plugin-proposal-class-properties/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-IobU0Xme31ewjYOShSIqd/ZGM/r/cuOz2z0MDbNrhF5FW+ZVgi0f2lyeoj9KFPDOAqsYxmLWZte1WOwlvY9aww==} engines: {node: '>=6.9.0'} @@ -3406,6 +3350,32 @@ packages: transitivePeerDependencies: - supports-color + /@babel/plugin-proposal-class-properties/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@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-plugin-utils': 7.19.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-class-properties/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@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-plugin-utils': 7.19.0 + transitivePeerDependencies: + - supports-color + dev: false + /@babel/plugin-proposal-class-properties/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} engines: {node: '>=6.9.0'} @@ -3418,34 +3388,6 @@ packages: transitivePeerDependencies: - supports-color - /@babel/plugin-proposal-class-static-block/7.17.6_@babel+core@7.12.9: - resolution: {integrity: sha512-X/tididvL2zbs7jZCeeRJ8167U/+Ac135AM6jCAx6gYXDUviZV5Ku9UDvWS2NCuWlFjIRXklYhwo6HhAC7ETnA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.12.0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.12.9 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-proposal-class-static-block/7.17.6_@babel+core@7.16.12: - resolution: {integrity: sha512-X/tididvL2zbs7jZCeeRJ8167U/+Ac135AM6jCAx6gYXDUviZV5Ku9UDvWS2NCuWlFjIRXklYhwo6HhAC7ETnA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.12.0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.16.12 - transitivePeerDependencies: - - supports-color - dev: false - /@babel/plugin-proposal-class-static-block/7.17.6_@babel+core@7.17.8: resolution: {integrity: sha512-X/tididvL2zbs7jZCeeRJ8167U/+Ac135AM6jCAx6gYXDUviZV5Ku9UDvWS2NCuWlFjIRXklYhwo6HhAC7ETnA==} engines: {node: '>=6.9.0'} @@ -3460,6 +3402,34 @@ packages: - supports-color dev: true + /@babel/plugin-proposal-class-static-block/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.12.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-class-static-block/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.16.12 + transitivePeerDependencies: + - supports-color + dev: false + /@babel/plugin-proposal-class-static-block/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==} engines: {node: '>=6.9.0'} @@ -3498,28 +3468,6 @@ packages: '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.12.9 dev: true - /@babel/plugin-proposal-dynamic-import/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.12.9 - dev: true - - /@babel/plugin-proposal-dynamic-import/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.16.12 - dev: false - /@babel/plugin-proposal-dynamic-import/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg==} engines: {node: '>=6.9.0'} @@ -3531,6 +3479,28 @@ packages: '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.17.8 dev: true + /@babel/plugin-proposal-dynamic-import/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.12.9 + dev: true + + /@babel/plugin-proposal-dynamic-import/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.16.12 + dev: false + /@babel/plugin-proposal-dynamic-import/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} engines: {node: '>=6.9.0'} @@ -3562,28 +3532,6 @@ packages: '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.12.9 dev: true - /@babel/plugin-proposal-export-namespace-from/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-ZxdtqDXLRGBL64ocZcs7ovt71L3jhC1RGSyR996svrCi3PYqHNkb3SwPJCs8RIzD86s+WPpt2S73+EHCGO+NUA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.12.9 - dev: true - - /@babel/plugin-proposal-export-namespace-from/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-ZxdtqDXLRGBL64ocZcs7ovt71L3jhC1RGSyR996svrCi3PYqHNkb3SwPJCs8RIzD86s+WPpt2S73+EHCGO+NUA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.16.12 - dev: false - /@babel/plugin-proposal-export-namespace-from/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-ZxdtqDXLRGBL64ocZcs7ovt71L3jhC1RGSyR996svrCi3PYqHNkb3SwPJCs8RIzD86s+WPpt2S73+EHCGO+NUA==} engines: {node: '>=6.9.0'} @@ -3595,6 +3543,28 @@ packages: '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.17.8 dev: true + /@babel/plugin-proposal-export-namespace-from/7.18.9_@babel+core@7.12.9: + resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.12.9 + dev: true + + /@babel/plugin-proposal-export-namespace-from/7.18.9_@babel+core@7.16.12: + resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.16.12 + dev: false + /@babel/plugin-proposal-export-namespace-from/7.18.9_@babel+core@7.17.8: resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} engines: {node: '>=6.9.0'} @@ -3616,28 +3586,6 @@ packages: '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.12.9 dev: true - /@babel/plugin-proposal-json-strings/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-lNZ3EEggsGY78JavgbHsK9u5P3pQaW7k4axlgFLYkMd7UBsiNahCITShLjNQschPyjtO6dADrL24757IdhBrsQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.12.9 - dev: true - - /@babel/plugin-proposal-json-strings/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-lNZ3EEggsGY78JavgbHsK9u5P3pQaW7k4axlgFLYkMd7UBsiNahCITShLjNQschPyjtO6dADrL24757IdhBrsQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.16.12 - dev: false - /@babel/plugin-proposal-json-strings/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-lNZ3EEggsGY78JavgbHsK9u5P3pQaW7k4axlgFLYkMd7UBsiNahCITShLjNQschPyjtO6dADrL24757IdhBrsQ==} engines: {node: '>=6.9.0'} @@ -3649,6 +3597,28 @@ packages: '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.17.8 dev: true + /@babel/plugin-proposal-json-strings/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.12.9 + dev: true + + /@babel/plugin-proposal-json-strings/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.16.12 + dev: false + /@babel/plugin-proposal-json-strings/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} engines: {node: '>=6.9.0'} @@ -3670,28 +3640,6 @@ packages: '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.12.9 dev: true - /@babel/plugin-proposal-logical-assignment-operators/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-K3XzyZJGQCr00+EtYtrDjmwX7o7PLK6U9bi1nCwkQioRFVUv6dJoxbQjtWVtP+bCPy82bONBKG8NPyQ4+i6yjg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.12.9 - dev: true - - /@babel/plugin-proposal-logical-assignment-operators/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-K3XzyZJGQCr00+EtYtrDjmwX7o7PLK6U9bi1nCwkQioRFVUv6dJoxbQjtWVtP+bCPy82bONBKG8NPyQ4+i6yjg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.16.12 - dev: false - /@babel/plugin-proposal-logical-assignment-operators/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-K3XzyZJGQCr00+EtYtrDjmwX7o7PLK6U9bi1nCwkQioRFVUv6dJoxbQjtWVtP+bCPy82bONBKG8NPyQ4+i6yjg==} engines: {node: '>=6.9.0'} @@ -3703,6 +3651,28 @@ packages: '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.17.8 dev: true + /@babel/plugin-proposal-logical-assignment-operators/7.18.9_@babel+core@7.12.9: + resolution: {integrity: sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.12.9 + dev: true + + /@babel/plugin-proposal-logical-assignment-operators/7.18.9_@babel+core@7.16.12: + resolution: {integrity: sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.16.12 + dev: false + /@babel/plugin-proposal-logical-assignment-operators/7.18.9_@babel+core@7.17.8: resolution: {integrity: sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==} engines: {node: '>=6.9.0'} @@ -3724,28 +3694,6 @@ packages: '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.12.9 dev: true - /@babel/plugin-proposal-nullish-coalescing-operator/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.12.9 - dev: true - - /@babel/plugin-proposal-nullish-coalescing-operator/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.16.12 - dev: false - /@babel/plugin-proposal-nullish-coalescing-operator/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ==} engines: {node: '>=6.9.0'} @@ -3756,6 +3704,28 @@ packages: '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.17.8 + /@babel/plugin-proposal-nullish-coalescing-operator/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.12.9 + dev: true + + /@babel/plugin-proposal-nullish-coalescing-operator/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.16.12 + dev: false + /@babel/plugin-proposal-nullish-coalescing-operator/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} engines: {node: '>=6.9.0'} @@ -3777,28 +3747,6 @@ packages: '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.12.9 dev: true - /@babel/plugin-proposal-numeric-separator/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.12.9 - dev: true - - /@babel/plugin-proposal-numeric-separator/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.16.12 - dev: false - /@babel/plugin-proposal-numeric-separator/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw==} engines: {node: '>=6.9.0'} @@ -3810,6 +3758,28 @@ packages: '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.17.8 dev: true + /@babel/plugin-proposal-numeric-separator/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.12.9 + dev: true + + /@babel/plugin-proposal-numeric-separator/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.16.12 + dev: false + /@babel/plugin-proposal-numeric-separator/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} engines: {node: '>=6.9.0'} @@ -3845,34 +3815,6 @@ packages: '@babel/plugin-transform-parameters': 7.16.3_@babel+core@7.12.9 dev: true - /@babel/plugin-proposal-object-rest-spread/7.17.3_@babel+core@7.12.9: - resolution: {integrity: sha512-yuL5iQA/TbZn+RGAfxQXfi7CNLmKi1f8zInn4IgobuCWcAb7i+zj4TYzQ9l8cEzVyJ89PDGuqxK1xZpUDISesw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.19.3 - '@babel/core': 7.12.9 - '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.12.9 - '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.12.9 - dev: true - - /@babel/plugin-proposal-object-rest-spread/7.17.3_@babel+core@7.16.12: - resolution: {integrity: sha512-yuL5iQA/TbZn+RGAfxQXfi7CNLmKi1f8zInn4IgobuCWcAb7i+zj4TYzQ9l8cEzVyJ89PDGuqxK1xZpUDISesw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.19.3 - '@babel/core': 7.16.12 - '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.16.12 - '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.16.12 - dev: false - /@babel/plugin-proposal-object-rest-spread/7.17.3_@babel+core@7.17.8: resolution: {integrity: sha512-yuL5iQA/TbZn+RGAfxQXfi7CNLmKi1f8zInn4IgobuCWcAb7i+zj4TYzQ9l8cEzVyJ89PDGuqxK1xZpUDISesw==} engines: {node: '>=6.9.0'} @@ -3887,6 +3829,34 @@ packages: '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.17.8 dev: true + /@babel/plugin-proposal-object-rest-spread/7.18.9_@babel+core@7.12.9: + resolution: {integrity: sha512-kDDHQ5rflIeY5xl69CEqGEZ0KY369ehsCIEbTGb4siHG5BE9sga/T0r0OUwyZNLMmZE79E1kbsqAjwFCW4ds6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.19.3 + '@babel/core': 7.12.9 + '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.12.9 + '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.12.9 + dev: true + + /@babel/plugin-proposal-object-rest-spread/7.18.9_@babel+core@7.16.12: + resolution: {integrity: sha512-kDDHQ5rflIeY5xl69CEqGEZ0KY369ehsCIEbTGb4siHG5BE9sga/T0r0OUwyZNLMmZE79E1kbsqAjwFCW4ds6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.19.3 + '@babel/core': 7.16.12 + '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.16.12 + '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.16.12 + dev: false + /@babel/plugin-proposal-object-rest-spread/7.18.9_@babel+core@7.17.8: resolution: {integrity: sha512-kDDHQ5rflIeY5xl69CEqGEZ0KY369ehsCIEbTGb4siHG5BE9sga/T0r0OUwyZNLMmZE79E1kbsqAjwFCW4ds6Q==} engines: {node: '>=6.9.0'} @@ -3911,28 +3881,6 @@ packages: '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.12.9 dev: true - /@babel/plugin-proposal-optional-catch-binding/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-eMOH/L4OvWSZAE1VkHbr1vckLG1WUcHGJSLqqQwl2GaUqG6QjddvrOaTUMNYiv77H5IKPMZ9U9P7EaHwvAShfA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.12.9 - dev: true - - /@babel/plugin-proposal-optional-catch-binding/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-eMOH/L4OvWSZAE1VkHbr1vckLG1WUcHGJSLqqQwl2GaUqG6QjddvrOaTUMNYiv77H5IKPMZ9U9P7EaHwvAShfA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.16.12 - dev: false - /@babel/plugin-proposal-optional-catch-binding/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-eMOH/L4OvWSZAE1VkHbr1vckLG1WUcHGJSLqqQwl2GaUqG6QjddvrOaTUMNYiv77H5IKPMZ9U9P7EaHwvAShfA==} engines: {node: '>=6.9.0'} @@ -3944,6 +3892,28 @@ packages: '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.17.8 dev: true + /@babel/plugin-proposal-optional-catch-binding/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.12.9 + dev: true + + /@babel/plugin-proposal-optional-catch-binding/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.16.12 + dev: false + /@babel/plugin-proposal-optional-catch-binding/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} engines: {node: '>=6.9.0'} @@ -3966,30 +3936,6 @@ packages: '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.12.9 dev: true - /@babel/plugin-proposal-optional-chaining/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 - '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.12.9 - dev: true - - /@babel/plugin-proposal-optional-chaining/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 - '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.16.12 - dev: false - /@babel/plugin-proposal-optional-chaining/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA==} engines: {node: '>=6.9.0'} @@ -4049,32 +3995,6 @@ packages: - supports-color dev: true - /@babel/plugin-proposal-private-methods/7.16.11_@babel+core@7.12.9: - resolution: {integrity: sha512-F/2uAkPlXDr8+BHpZvo19w3hLFKge+k75XUprE6jaqKxjGkSYcK+4c+bup5PdW/7W/Rpjwql7FTVEDW+fRAQsw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-proposal-private-methods/7.16.11_@babel+core@7.16.12: - resolution: {integrity: sha512-F/2uAkPlXDr8+BHpZvo19w3hLFKge+k75XUprE6jaqKxjGkSYcK+4c+bup5PdW/7W/Rpjwql7FTVEDW+fRAQsw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.18.9 - transitivePeerDependencies: - - supports-color - dev: false - /@babel/plugin-proposal-private-methods/7.16.11_@babel+core@7.17.8: resolution: {integrity: sha512-F/2uAkPlXDr8+BHpZvo19w3hLFKge+k75XUprE6jaqKxjGkSYcK+4c+bup5PdW/7W/Rpjwql7FTVEDW+fRAQsw==} engines: {node: '>=6.9.0'} @@ -4088,6 +4008,32 @@ packages: - supports-color dev: true + /@babel/plugin-proposal-private-methods/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@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-plugin-utils': 7.19.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-private-methods/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@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-plugin-utils': 7.19.0 + transitivePeerDependencies: + - supports-color + dev: false + /@babel/plugin-proposal-private-methods/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} engines: {node: '>=6.9.0'} @@ -4100,36 +4046,6 @@ packages: transitivePeerDependencies: - supports-color - /@babel/plugin-proposal-private-property-in-object/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-rMQkjcOFbm+ufe3bTZLyOfsOUOxyvLXZJCTARhJr+8UMSoZmqTe1K1BgkFcrW37rAchWg57yI69ORxiWvUINuQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-annotate-as-pure': 7.16.7 - '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 - '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.12.9 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-proposal-private-property-in-object/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-rMQkjcOFbm+ufe3bTZLyOfsOUOxyvLXZJCTARhJr+8UMSoZmqTe1K1BgkFcrW37rAchWg57yI69ORxiWvUINuQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-annotate-as-pure': 7.16.7 - '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.18.9 - '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.16.12 - transitivePeerDependencies: - - supports-color - dev: false - /@babel/plugin-proposal-private-property-in-object/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-rMQkjcOFbm+ufe3bTZLyOfsOUOxyvLXZJCTARhJr+8UMSoZmqTe1K1BgkFcrW37rAchWg57yI69ORxiWvUINuQ==} engines: {node: '>=6.9.0'} @@ -4145,6 +4061,36 @@ packages: - supports-color dev: true + /@babel/plugin-proposal-private-property-in-object/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.12.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-private-property-in-object/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.16.12 + transitivePeerDependencies: + - supports-color + dev: false + /@babel/plugin-proposal-private-property-in-object/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==} engines: {node: '>=6.9.0'} @@ -4170,28 +4116,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-proposal-unicode-property-regex/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-QRK0YI/40VLhNVGIjRNAAQkEHws0cswSdFFjpFyt943YmJIU1da9uW63Iu6NFV6CxTZW5eTDCrwZUstBWgp/Rg==} - engines: {node: '>=4'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-proposal-unicode-property-regex/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-QRK0YI/40VLhNVGIjRNAAQkEHws0cswSdFFjpFyt943YmJIU1da9uW63Iu6NFV6CxTZW5eTDCrwZUstBWgp/Rg==} - engines: {node: '>=4'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-proposal-unicode-property-regex/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-QRK0YI/40VLhNVGIjRNAAQkEHws0cswSdFFjpFyt943YmJIU1da9uW63Iu6NFV6CxTZW5eTDCrwZUstBWgp/Rg==} engines: {node: '>=4'} @@ -4755,26 +4679,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-arrow-functions/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-9ffkFFMbvzTvv+7dTp/66xvZAWASuPD5Tl9LK3Z9vhOmANo6j94rik+5YMBt4CwHVMWLWpMsriIc2zsa3WW3xQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-arrow-functions/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-9ffkFFMbvzTvv+7dTp/66xvZAWASuPD5Tl9LK3Z9vhOmANo6j94rik+5YMBt4CwHVMWLWpMsriIc2zsa3WW3xQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-arrow-functions/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-9ffkFFMbvzTvv+7dTp/66xvZAWASuPD5Tl9LK3Z9vhOmANo6j94rik+5YMBt4CwHVMWLWpMsriIc2zsa3WW3xQ==} engines: {node: '>=6.9.0'} @@ -4785,6 +4689,26 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-arrow-functions/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-arrow-functions/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-arrow-functions/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==} engines: {node: '>=6.9.0'} @@ -4808,34 +4732,6 @@ packages: - supports-color dev: true - /@babel/plugin-transform-async-to-generator/7.16.8_@babel+core@7.12.9: - resolution: {integrity: sha512-MtmUmTJQHCnyJVrScNzNlofQJ3dLFuobYn3mwOTKHnSCMtbNsqvF71GQmJfFjdrXSsAA7iysFmYWw4bXZ20hOg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-module-imports': 7.16.7 - '@babel/helper-plugin-utils': 7.18.9 - '@babel/helper-remap-async-to-generator': 7.16.8 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-async-to-generator/7.16.8_@babel+core@7.16.12: - resolution: {integrity: sha512-MtmUmTJQHCnyJVrScNzNlofQJ3dLFuobYn3mwOTKHnSCMtbNsqvF71GQmJfFjdrXSsAA7iysFmYWw4bXZ20hOg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-module-imports': 7.16.7 - '@babel/helper-plugin-utils': 7.18.9 - '@babel/helper-remap-async-to-generator': 7.16.8 - transitivePeerDependencies: - - supports-color - dev: false - /@babel/plugin-transform-async-to-generator/7.16.8_@babel+core@7.17.8: resolution: {integrity: sha512-MtmUmTJQHCnyJVrScNzNlofQJ3dLFuobYn3mwOTKHnSCMtbNsqvF71GQmJfFjdrXSsAA7iysFmYWw4bXZ20hOg==} engines: {node: '>=6.9.0'} @@ -4850,6 +4746,34 @@ packages: - supports-color dev: true + /@babel/plugin-transform-async-to-generator/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-module-imports': 7.18.6 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.12.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-async-to-generator/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-module-imports': 7.18.6 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.16.12 + transitivePeerDependencies: + - supports-color + dev: false + /@babel/plugin-transform-async-to-generator/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==} engines: {node: '>=6.9.0'} @@ -4873,26 +4797,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-block-scoped-functions/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-JUuzlzmF40Z9cXyytcbZEZKckgrQzChbQJw/5PuEHYeqzCsvebDx0K0jWnIIVcmmDOAVctCgnYs0pMcrYj2zJg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-block-scoped-functions/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-JUuzlzmF40Z9cXyytcbZEZKckgrQzChbQJw/5PuEHYeqzCsvebDx0K0jWnIIVcmmDOAVctCgnYs0pMcrYj2zJg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-block-scoped-functions/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-JUuzlzmF40Z9cXyytcbZEZKckgrQzChbQJw/5PuEHYeqzCsvebDx0K0jWnIIVcmmDOAVctCgnYs0pMcrYj2zJg==} engines: {node: '>=6.9.0'} @@ -4903,6 +4807,26 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-block-scoped-functions/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-block-scoped-functions/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-block-scoped-functions/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==} engines: {node: '>=6.9.0'} @@ -4922,26 +4846,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-block-scoping/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-ObZev2nxVAYA4bhyusELdo9hb3H+A56bxH3FZMbEImZFiEDYVHXQSJ1hQKFlDnlt8G9bBrCZ5ZpURZUrV4G5qQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-block-scoping/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-ObZev2nxVAYA4bhyusELdo9hb3H+A56bxH3FZMbEImZFiEDYVHXQSJ1hQKFlDnlt8G9bBrCZ5ZpURZUrV4G5qQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-block-scoping/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-ObZev2nxVAYA4bhyusELdo9hb3H+A56bxH3FZMbEImZFiEDYVHXQSJ1hQKFlDnlt8G9bBrCZ5ZpURZUrV4G5qQ==} engines: {node: '>=6.9.0'} @@ -4952,6 +4856,26 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-block-scoping/7.18.9_@babel+core@7.12.9: + resolution: {integrity: sha512-5sDIJRV1KtQVEbt/EIBwGy4T01uYIo4KRB3VUqzkhrAIOGx7AoctL9+Ux88btY0zXdDyPJ9mW+bg+v+XEkGmtw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-block-scoping/7.18.9_@babel+core@7.16.12: + resolution: {integrity: sha512-5sDIJRV1KtQVEbt/EIBwGy4T01uYIo4KRB3VUqzkhrAIOGx7AoctL9+Ux88btY0zXdDyPJ9mW+bg+v+XEkGmtw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-block-scoping/7.18.9_@babel+core@7.17.8: resolution: {integrity: sha512-5sDIJRV1KtQVEbt/EIBwGy4T01uYIo4KRB3VUqzkhrAIOGx7AoctL9+Ux88btY0zXdDyPJ9mW+bg+v+XEkGmtw==} engines: {node: '>=6.9.0'} @@ -4979,44 +4903,6 @@ packages: - supports-color dev: true - /@babel/plugin-transform-classes/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-WY7og38SFAGYRe64BrjKf8OrE6ulEHtr5jEYaZMwox9KebgqPi67Zqz8K53EKk1fFEJgm96r32rkKZ3qA2nCWQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-optimise-call-expression': 7.18.6 - '@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 - dev: true - - /@babel/plugin-transform-classes/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-WY7og38SFAGYRe64BrjKf8OrE6ulEHtr5jEYaZMwox9KebgqPi67Zqz8K53EKk1fFEJgm96r32rkKZ3qA2nCWQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-optimise-call-expression': 7.18.6 - '@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 - dev: false - /@babel/plugin-transform-classes/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-WY7og38SFAGYRe64BrjKf8OrE6ulEHtr5jEYaZMwox9KebgqPi67Zqz8K53EKk1fFEJgm96r32rkKZ3qA2nCWQ==} engines: {node: '>=6.9.0'} @@ -5036,6 +4922,46 @@ packages: - supports-color dev: true + /@babel/plugin-transform-classes/7.19.0_@babel+core@7.12.9: + resolution: {integrity: sha512-YfeEE9kCjqTS9IitkgfJuxjcEtLUHMqa8yUJ6zdz8vR7hKuo6mOy2C05P0F1tdMmDCeuyidKnlrw/iTppHcr2A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.12.9 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-function-name': 7.19.0 + '@babel/helper-optimise-call-expression': 7.18.6 + '@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 + dev: true + + /@babel/plugin-transform-classes/7.19.0_@babel+core@7.16.12: + resolution: {integrity: sha512-YfeEE9kCjqTS9IitkgfJuxjcEtLUHMqa8yUJ6zdz8vR7hKuo6mOy2C05P0F1tdMmDCeuyidKnlrw/iTppHcr2A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.16.12 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-function-name': 7.19.0 + '@babel/helper-optimise-call-expression': 7.18.6 + '@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 + dev: false + /@babel/plugin-transform-classes/7.19.0_@babel+core@7.17.8: resolution: {integrity: sha512-YfeEE9kCjqTS9IitkgfJuxjcEtLUHMqa8yUJ6zdz8vR7hKuo6mOy2C05P0F1tdMmDCeuyidKnlrw/iTppHcr2A==} engines: {node: '>=6.9.0'} @@ -5065,26 +4991,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-computed-properties/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-gN72G9bcmenVILj//sv1zLNaPyYcOzUho2lIJBMh/iakJ9ygCo/hEF9cpGb61SCMEDxbbyBoVQxrt+bWKu5KGw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-computed-properties/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-gN72G9bcmenVILj//sv1zLNaPyYcOzUho2lIJBMh/iakJ9ygCo/hEF9cpGb61SCMEDxbbyBoVQxrt+bWKu5KGw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-computed-properties/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-gN72G9bcmenVILj//sv1zLNaPyYcOzUho2lIJBMh/iakJ9ygCo/hEF9cpGb61SCMEDxbbyBoVQxrt+bWKu5KGw==} engines: {node: '>=6.9.0'} @@ -5095,6 +5001,26 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-computed-properties/7.18.9_@babel+core@7.12.9: + resolution: {integrity: sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-computed-properties/7.18.9_@babel+core@7.16.12: + resolution: {integrity: sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-computed-properties/7.18.9_@babel+core@7.17.8: resolution: {integrity: sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==} engines: {node: '>=6.9.0'} @@ -5114,26 +5040,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-destructuring/7.17.7_@babel+core@7.12.9: - resolution: {integrity: sha512-XVh0r5yq9sLR4vZ6eVZe8FKfIcSgaTBxVBRSYokRj2qksf6QerYnTxz9/GTuKTH/n/HwLP7t6gtlybHetJ/6hQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-destructuring/7.17.7_@babel+core@7.16.12: - resolution: {integrity: sha512-XVh0r5yq9sLR4vZ6eVZe8FKfIcSgaTBxVBRSYokRj2qksf6QerYnTxz9/GTuKTH/n/HwLP7t6gtlybHetJ/6hQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-destructuring/7.17.7_@babel+core@7.17.8: resolution: {integrity: sha512-XVh0r5yq9sLR4vZ6eVZe8FKfIcSgaTBxVBRSYokRj2qksf6QerYnTxz9/GTuKTH/n/HwLP7t6gtlybHetJ/6hQ==} engines: {node: '>=6.9.0'} @@ -5144,6 +5050,26 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-destructuring/7.18.13_@babel+core@7.12.9: + resolution: {integrity: sha512-TodpQ29XekIsex2A+YJPj5ax2plkGa8YYY6mFjCohk/IG9IY42Rtuj1FuDeemfg2ipxIFLzPeA83SIBnlhSIow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-destructuring/7.18.13_@babel+core@7.16.12: + resolution: {integrity: sha512-TodpQ29XekIsex2A+YJPj5ax2plkGa8YYY6mFjCohk/IG9IY42Rtuj1FuDeemfg2ipxIFLzPeA83SIBnlhSIow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-destructuring/7.18.13_@babel+core@7.17.8: resolution: {integrity: sha512-TodpQ29XekIsex2A+YJPj5ax2plkGa8YYY6mFjCohk/IG9IY42Rtuj1FuDeemfg2ipxIFLzPeA83SIBnlhSIow==} engines: {node: '>=6.9.0'} @@ -5164,28 +5090,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-dotall-regex/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-Lyttaao2SjZF6Pf4vk1dVKv8YypMpomAbygW+mU5cYP3S5cWTfCJjG8xV6CFdzGFlfWK81IjL9viiTvpb6G7gQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-dotall-regex/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-Lyttaao2SjZF6Pf4vk1dVKv8YypMpomAbygW+mU5cYP3S5cWTfCJjG8xV6CFdzGFlfWK81IjL9viiTvpb6G7gQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-dotall-regex/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-Lyttaao2SjZF6Pf4vk1dVKv8YypMpomAbygW+mU5cYP3S5cWTfCJjG8xV6CFdzGFlfWK81IjL9viiTvpb6G7gQ==} engines: {node: '>=6.9.0'} @@ -5239,26 +5143,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-duplicate-keys/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-03DvpbRfvWIXyK0/6QiR1KMTWeT6OcQ7tbhjrXyFS02kjuX/mu5Bvnh5SDSWHxyawit2g5aWhKwI86EE7GUnTw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-duplicate-keys/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-03DvpbRfvWIXyK0/6QiR1KMTWeT6OcQ7tbhjrXyFS02kjuX/mu5Bvnh5SDSWHxyawit2g5aWhKwI86EE7GUnTw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-duplicate-keys/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-03DvpbRfvWIXyK0/6QiR1KMTWeT6OcQ7tbhjrXyFS02kjuX/mu5Bvnh5SDSWHxyawit2g5aWhKwI86EE7GUnTw==} engines: {node: '>=6.9.0'} @@ -5269,6 +5153,26 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-duplicate-keys/7.18.9_@babel+core@7.12.9: + resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-duplicate-keys/7.18.9_@babel+core@7.16.12: + resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-duplicate-keys/7.18.9_@babel+core@7.17.8: resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==} engines: {node: '>=6.9.0'} @@ -5289,28 +5193,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-exponentiation-operator/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-8UYLSlyLgRixQvlYH3J2ekXFHDFLQutdy7FfFAMm3CPZ6q9wHCwnUyiXpQCe3gVVnQlHc5nsuiEVziteRNTXEA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-exponentiation-operator/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-8UYLSlyLgRixQvlYH3J2ekXFHDFLQutdy7FfFAMm3CPZ6q9wHCwnUyiXpQCe3gVVnQlHc5nsuiEVziteRNTXEA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-exponentiation-operator/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-8UYLSlyLgRixQvlYH3J2ekXFHDFLQutdy7FfFAMm3CPZ6q9wHCwnUyiXpQCe3gVVnQlHc5nsuiEVziteRNTXEA==} engines: {node: '>=6.9.0'} @@ -5322,6 +5204,28 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-exponentiation-operator/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-exponentiation-operator/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-exponentiation-operator/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==} engines: {node: '>=6.9.0'} @@ -5352,26 +5256,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-for-of/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-/QZm9W92Ptpw7sjI9Nx1mbcsWz33+l8kuMIQnDwgQBG5s3fAfQvkRjQ7NqXhtNcKOnPkdICmUHyCaWW06HCsqg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-for-of/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-/QZm9W92Ptpw7sjI9Nx1mbcsWz33+l8kuMIQnDwgQBG5s3fAfQvkRjQ7NqXhtNcKOnPkdICmUHyCaWW06HCsqg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-for-of/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-/QZm9W92Ptpw7sjI9Nx1mbcsWz33+l8kuMIQnDwgQBG5s3fAfQvkRjQ7NqXhtNcKOnPkdICmUHyCaWW06HCsqg==} engines: {node: '>=6.9.0'} @@ -5382,6 +5266,26 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-for-of/7.18.8_@babel+core@7.12.9: + resolution: {integrity: sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-for-of/7.18.8_@babel+core@7.16.12: + resolution: {integrity: sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-for-of/7.18.8_@babel+core@7.17.8: resolution: {integrity: sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==} engines: {node: '>=6.9.0'} @@ -5402,30 +5306,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-function-name/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-SU/C68YVwTRxqWj5kgsbKINakGag0KTgq9f2iZEXdStoAbOzLHEBRYzImmA6yFo8YZhJVflvXmIHUO7GWHmxxA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.12.9 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-function-name/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-SU/C68YVwTRxqWj5kgsbKINakGag0KTgq9f2iZEXdStoAbOzLHEBRYzImmA6yFo8YZhJVflvXmIHUO7GWHmxxA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.16.12 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-function-name/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-SU/C68YVwTRxqWj5kgsbKINakGag0KTgq9f2iZEXdStoAbOzLHEBRYzImmA6yFo8YZhJVflvXmIHUO7GWHmxxA==} engines: {node: '>=6.9.0'} @@ -5438,6 +5318,30 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-function-name/7.18.9_@babel+core@7.12.9: + resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.12.9 + '@babel/helper-function-name': 7.19.0 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-function-name/7.18.9_@babel+core@7.16.12: + resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.16.12 + '@babel/helper-function-name': 7.19.0 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-function-name/7.18.9_@babel+core@7.17.8: resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==} engines: {node: '>=6.9.0'} @@ -5459,26 +5363,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-literals/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-6tH8RTpTWI0s2sV6uq3e/C9wPo4PTqqZps4uF0kzQ9/xPLFQtipynvmT1g/dOfEJ+0EQsHhkQ/zyRId8J2b8zQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-literals/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-6tH8RTpTWI0s2sV6uq3e/C9wPo4PTqqZps4uF0kzQ9/xPLFQtipynvmT1g/dOfEJ+0EQsHhkQ/zyRId8J2b8zQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-literals/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-6tH8RTpTWI0s2sV6uq3e/C9wPo4PTqqZps4uF0kzQ9/xPLFQtipynvmT1g/dOfEJ+0EQsHhkQ/zyRId8J2b8zQ==} engines: {node: '>=6.9.0'} @@ -5489,6 +5373,26 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-literals/7.18.9_@babel+core@7.12.9: + resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-literals/7.18.9_@babel+core@7.16.12: + resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-literals/7.18.9_@babel+core@7.17.8: resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==} engines: {node: '>=6.9.0'} @@ -5508,26 +5412,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-member-expression-literals/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-mBruRMbktKQwbxaJof32LT9KLy2f3gH+27a5XSuXo6h7R3vqltl0PgZ80C8ZMKw98Bf8bqt6BEVi3svOh2PzMw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-member-expression-literals/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-mBruRMbktKQwbxaJof32LT9KLy2f3gH+27a5XSuXo6h7R3vqltl0PgZ80C8ZMKw98Bf8bqt6BEVi3svOh2PzMw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-member-expression-literals/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-mBruRMbktKQwbxaJof32LT9KLy2f3gH+27a5XSuXo6h7R3vqltl0PgZ80C8ZMKw98Bf8bqt6BEVi3svOh2PzMw==} engines: {node: '>=6.9.0'} @@ -5538,6 +5422,26 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-member-expression-literals/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-member-expression-literals/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-member-expression-literals/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==} engines: {node: '>=6.9.0'} @@ -5561,34 +5465,6 @@ packages: - supports-color dev: true - /@babel/plugin-transform-modules-amd/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-KaaEtgBL7FKYwjJ/teH63oAmE3lP34N3kshz8mm4VMAw7U3PxjVwwUmxEFksbgsNUaO3wId9R2AVQYSEGRa2+g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.19.0 - babel-plugin-dynamic-import-node: 2.3.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-modules-amd/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-KaaEtgBL7FKYwjJ/teH63oAmE3lP34N3kshz8mm4VMAw7U3PxjVwwUmxEFksbgsNUaO3wId9R2AVQYSEGRa2+g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.19.0 - babel-plugin-dynamic-import-node: 2.3.3 - transitivePeerDependencies: - - supports-color - dev: false - /@babel/plugin-transform-modules-amd/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-KaaEtgBL7FKYwjJ/teH63oAmE3lP34N3kshz8mm4VMAw7U3PxjVwwUmxEFksbgsNUaO3wId9R2AVQYSEGRa2+g==} engines: {node: '>=6.9.0'} @@ -5603,6 +5479,34 @@ packages: - supports-color dev: true + /@babel/plugin-transform-modules-amd/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-Pra5aXsmTsOnjM3IajS8rTaLCy++nGM4v3YR4esk5PCsyg9z8NA5oQLwxzMUtDBd8F+UmVza3VxoAaWCbzH1rg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-module-transforms': 7.19.0 + '@babel/helper-plugin-utils': 7.19.0 + babel-plugin-dynamic-import-node: 2.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-amd/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-Pra5aXsmTsOnjM3IajS8rTaLCy++nGM4v3YR4esk5PCsyg9z8NA5oQLwxzMUtDBd8F+UmVza3VxoAaWCbzH1rg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-module-transforms': 7.19.0 + '@babel/helper-plugin-utils': 7.19.0 + babel-plugin-dynamic-import-node: 2.3.3 + transitivePeerDependencies: + - supports-color + dev: false + /@babel/plugin-transform-modules-amd/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-Pra5aXsmTsOnjM3IajS8rTaLCy++nGM4v3YR4esk5PCsyg9z8NA5oQLwxzMUtDBd8F+UmVza3VxoAaWCbzH1rg==} engines: {node: '>=6.9.0'} @@ -5631,36 +5535,6 @@ packages: - supports-color dev: true - /@babel/plugin-transform-modules-commonjs/7.17.7_@babel+core@7.12.9: - resolution: {integrity: sha512-ITPmR2V7MqioMJyrxUo2onHNC3e+MvfFiFIR0RP21d3PtlVb6sfzoxNKiphSZUOM9hEIdzCcZe83ieX3yoqjUA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-simple-access': 7.18.6 - babel-plugin-dynamic-import-node: 2.3.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-modules-commonjs/7.17.7_@babel+core@7.16.12: - resolution: {integrity: sha512-ITPmR2V7MqioMJyrxUo2onHNC3e+MvfFiFIR0RP21d3PtlVb6sfzoxNKiphSZUOM9hEIdzCcZe83ieX3yoqjUA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-simple-access': 7.18.6 - babel-plugin-dynamic-import-node: 2.3.3 - transitivePeerDependencies: - - supports-color - dev: false - /@babel/plugin-transform-modules-commonjs/7.17.7_@babel+core@7.17.8: resolution: {integrity: sha512-ITPmR2V7MqioMJyrxUo2onHNC3e+MvfFiFIR0RP21d3PtlVb6sfzoxNKiphSZUOM9hEIdzCcZe83ieX3yoqjUA==} engines: {node: '>=6.9.0'} @@ -5675,6 +5549,36 @@ packages: transitivePeerDependencies: - supports-color + /@babel/plugin-transform-modules-commonjs/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-Qfv2ZOWikpvmedXQJDSbxNqy7Xr/j2Y8/KfijM0iJyKkBTmWuvCA1yeH1yDM7NJhBW/2aXxeucLj6i80/LAJ/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-module-transforms': 7.19.0 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-simple-access': 7.18.6 + babel-plugin-dynamic-import-node: 2.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-commonjs/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-Qfv2ZOWikpvmedXQJDSbxNqy7Xr/j2Y8/KfijM0iJyKkBTmWuvCA1yeH1yDM7NJhBW/2aXxeucLj6i80/LAJ/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-module-transforms': 7.19.0 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-simple-access': 7.18.6 + babel-plugin-dynamic-import-node: 2.3.3 + transitivePeerDependencies: + - supports-color + dev: false + /@babel/plugin-transform-modules-commonjs/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-Qfv2ZOWikpvmedXQJDSbxNqy7Xr/j2Y8/KfijM0iJyKkBTmWuvCA1yeH1yDM7NJhBW/2aXxeucLj6i80/LAJ/Q==} engines: {node: '>=6.9.0'} @@ -5705,38 +5609,6 @@ packages: - supports-color dev: true - /@babel/plugin-transform-modules-systemjs/7.17.8_@babel+core@7.12.9: - resolution: {integrity: sha512-39reIkMTUVagzgA5x88zDYXPCMT6lcaRKs1+S9K6NKBPErbgO/w/kP8GlNQTC87b412ZTlmNgr3k2JrWgHH+Bw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-validator-identifier': 7.19.1 - babel-plugin-dynamic-import-node: 2.3.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-modules-systemjs/7.17.8_@babel+core@7.16.12: - resolution: {integrity: sha512-39reIkMTUVagzgA5x88zDYXPCMT6lcaRKs1+S9K6NKBPErbgO/w/kP8GlNQTC87b412ZTlmNgr3k2JrWgHH+Bw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-validator-identifier': 7.19.1 - babel-plugin-dynamic-import-node: 2.3.3 - transitivePeerDependencies: - - supports-color - dev: false - /@babel/plugin-transform-modules-systemjs/7.17.8_@babel+core@7.17.8: resolution: {integrity: sha512-39reIkMTUVagzgA5x88zDYXPCMT6lcaRKs1+S9K6NKBPErbgO/w/kP8GlNQTC87b412ZTlmNgr3k2JrWgHH+Bw==} engines: {node: '>=6.9.0'} @@ -5753,6 +5625,38 @@ packages: - supports-color dev: true + /@babel/plugin-transform-modules-systemjs/7.19.0_@babel+core@7.12.9: + resolution: {integrity: sha512-x9aiR0WXAWmOWsqcsnrzGR+ieaTMVyGyffPVA7F8cXAGt/UxefYv6uSHZLkAFChN5M5Iy1+wjE+xJuPt22H39A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-hoist-variables': 7.18.6 + '@babel/helper-module-transforms': 7.19.0 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-validator-identifier': 7.19.1 + babel-plugin-dynamic-import-node: 2.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-systemjs/7.19.0_@babel+core@7.16.12: + resolution: {integrity: sha512-x9aiR0WXAWmOWsqcsnrzGR+ieaTMVyGyffPVA7F8cXAGt/UxefYv6uSHZLkAFChN5M5Iy1+wjE+xJuPt22H39A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-hoist-variables': 7.18.6 + '@babel/helper-module-transforms': 7.19.0 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-validator-identifier': 7.19.1 + babel-plugin-dynamic-import-node: 2.3.3 + transitivePeerDependencies: + - supports-color + dev: false + /@babel/plugin-transform-modules-systemjs/7.19.0_@babel+core@7.17.8: resolution: {integrity: sha512-x9aiR0WXAWmOWsqcsnrzGR+ieaTMVyGyffPVA7F8cXAGt/UxefYv6uSHZLkAFChN5M5Iy1+wjE+xJuPt22H39A==} engines: {node: '>=6.9.0'} @@ -5781,32 +5685,6 @@ packages: - supports-color dev: true - /@babel/plugin-transform-modules-umd/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-EMh7uolsC8O4xhudF2F6wedbSHm1HHZ0C6aJ7K67zcDNidMzVcxWdGr+htW9n21klm+bOn+Rx4CBsAntZd3rEQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.19.0 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-modules-umd/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-EMh7uolsC8O4xhudF2F6wedbSHm1HHZ0C6aJ7K67zcDNidMzVcxWdGr+htW9n21klm+bOn+Rx4CBsAntZd3rEQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.19.0 - transitivePeerDependencies: - - supports-color - dev: false - /@babel/plugin-transform-modules-umd/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-EMh7uolsC8O4xhudF2F6wedbSHm1HHZ0C6aJ7K67zcDNidMzVcxWdGr+htW9n21klm+bOn+Rx4CBsAntZd3rEQ==} engines: {node: '>=6.9.0'} @@ -5820,6 +5698,32 @@ packages: - supports-color dev: true + /@babel/plugin-transform-modules-umd/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-module-transforms': 7.19.0 + '@babel/helper-plugin-utils': 7.19.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-umd/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-module-transforms': 7.19.0 + '@babel/helper-plugin-utils': 7.19.0 + transitivePeerDependencies: + - supports-color + dev: false + /@babel/plugin-transform-modules-umd/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==} engines: {node: '>=6.9.0'} @@ -5842,26 +5746,6 @@ packages: '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.12.9 dev: true - /@babel/plugin-transform-named-capturing-groups-regex/7.16.8_@babel+core@7.12.9: - resolution: {integrity: sha512-j3Jw+n5PvpmhRR+mrgIh04puSANCk/T/UA3m3P1MjJkhlK906+ApHhDIqBQDdOgL/r1UYpz4GNclTXxyZrYGSw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.12.9 - dev: true - - /@babel/plugin-transform-named-capturing-groups-regex/7.16.8_@babel+core@7.16.12: - resolution: {integrity: sha512-j3Jw+n5PvpmhRR+mrgIh04puSANCk/T/UA3m3P1MjJkhlK906+ApHhDIqBQDdOgL/r1UYpz4GNclTXxyZrYGSw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.16.12 - dev: false - /@babel/plugin-transform-named-capturing-groups-regex/7.16.8_@babel+core@7.17.8: resolution: {integrity: sha512-j3Jw+n5PvpmhRR+mrgIh04puSANCk/T/UA3m3P1MjJkhlK906+ApHhDIqBQDdOgL/r1UYpz4GNclTXxyZrYGSw==} engines: {node: '>=6.9.0'} @@ -5872,6 +5756,28 @@ packages: '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.17.8 dev: true + /@babel/plugin-transform-named-capturing-groups-regex/7.19.1_@babel+core@7.12.9: + resolution: {integrity: sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-named-capturing-groups-regex/7.19.1_@babel+core@7.16.12: + resolution: {integrity: sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-named-capturing-groups-regex/7.19.1_@babel+core@7.17.8: resolution: {integrity: sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw==} engines: {node: '>=6.9.0'} @@ -5892,26 +5798,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-new-target/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-xiLDzWNMfKoGOpc6t3U+etCE2yRnn3SM09BXqWPIZOBpL2gvVrBWUKnsJx0K/ADi5F5YC5f8APFfWrz25TdlGg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-new-target/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-xiLDzWNMfKoGOpc6t3U+etCE2yRnn3SM09BXqWPIZOBpL2gvVrBWUKnsJx0K/ADi5F5YC5f8APFfWrz25TdlGg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-new-target/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-xiLDzWNMfKoGOpc6t3U+etCE2yRnn3SM09BXqWPIZOBpL2gvVrBWUKnsJx0K/ADi5F5YC5f8APFfWrz25TdlGg==} engines: {node: '>=6.9.0'} @@ -5922,6 +5808,26 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-new-target/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-new-target/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-new-target/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==} engines: {node: '>=6.9.0'} @@ -5944,32 +5850,6 @@ packages: - supports-color dev: true - /@babel/plugin-transform-object-super/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-14J1feiQVWaGvRxj2WjyMuXS2jsBkgB3MdSN5HuC2G5nRspa5RK9COcs82Pwy5BuGcjb+fYaUj94mYcOj7rCvw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-replace-supers': 7.19.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/plugin-transform-object-super/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-14J1feiQVWaGvRxj2WjyMuXS2jsBkgB3MdSN5HuC2G5nRspa5RK9COcs82Pwy5BuGcjb+fYaUj94mYcOj7rCvw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-replace-supers': 7.19.1 - transitivePeerDependencies: - - supports-color - dev: false - /@babel/plugin-transform-object-super/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-14J1feiQVWaGvRxj2WjyMuXS2jsBkgB3MdSN5HuC2G5nRspa5RK9COcs82Pwy5BuGcjb+fYaUj94mYcOj7rCvw==} engines: {node: '>=6.9.0'} @@ -5983,6 +5863,32 @@ packages: - supports-color dev: true + /@babel/plugin-transform-object-super/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-replace-supers': 7.19.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-object-super/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-replace-supers': 7.19.1 + transitivePeerDependencies: + - supports-color + dev: false + /@babel/plugin-transform-object-super/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==} engines: {node: '>=6.9.0'} @@ -6005,26 +5911,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-parameters/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-AT3MufQ7zZEhU2hwOA11axBnExW0Lszu4RL/tAlUJBuNoRak+wehQW8h6KcXOcgjY42fHtDxswuMhMjFEuv/aw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-parameters/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-AT3MufQ7zZEhU2hwOA11axBnExW0Lszu4RL/tAlUJBuNoRak+wehQW8h6KcXOcgjY42fHtDxswuMhMjFEuv/aw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-parameters/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-AT3MufQ7zZEhU2hwOA11axBnExW0Lszu4RL/tAlUJBuNoRak+wehQW8h6KcXOcgjY42fHtDxswuMhMjFEuv/aw==} engines: {node: '>=6.9.0'} @@ -6074,26 +5960,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-property-literals/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-z4FGr9NMGdoIl1RqavCqGG+ZuYjfZ/hkCIeuH6Do7tXmSm0ls11nYVSJqFEUOSJbDab5wC6lRE/w6YjVcr6Hqw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-property-literals/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-z4FGr9NMGdoIl1RqavCqGG+ZuYjfZ/hkCIeuH6Do7tXmSm0ls11nYVSJqFEUOSJbDab5wC6lRE/w6YjVcr6Hqw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-property-literals/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-z4FGr9NMGdoIl1RqavCqGG+ZuYjfZ/hkCIeuH6Do7tXmSm0ls11nYVSJqFEUOSJbDab5wC6lRE/w6YjVcr6Hqw==} engines: {node: '>=6.9.0'} @@ -6104,6 +5970,26 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-property-literals/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-property-literals/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-property-literals/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==} engines: {node: '>=6.9.0'} @@ -6185,7 +6071,7 @@ packages: '@babel/helper-module-imports': 7.16.0 '@babel/helper-plugin-utils': 7.14.5 '@babel/plugin-syntax-jsx': 7.16.0_@babel+core@7.16.12 - '@babel/types': 7.16.0 + '@babel/types': 7.19.3 dev: false /@babel/plugin-transform-react-jsx/7.17.3_@babel+core@7.17.8: @@ -6236,26 +6122,6 @@ packages: regenerator-transform: 0.14.5 dev: true - /@babel/plugin-transform-regenerator/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-mF7jOgGYCkSJagJ6XCujSQg+6xC1M77/03K2oBmVJWoFGNUtnVJO4WHKJk3dnPC8HCcj4xBQP1Egm8DWh3Pb3Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - regenerator-transform: 0.14.5 - dev: true - - /@babel/plugin-transform-regenerator/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-mF7jOgGYCkSJagJ6XCujSQg+6xC1M77/03K2oBmVJWoFGNUtnVJO4WHKJk3dnPC8HCcj4xBQP1Egm8DWh3Pb3Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - regenerator-transform: 0.14.5 - dev: false - /@babel/plugin-transform-regenerator/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-mF7jOgGYCkSJagJ6XCujSQg+6xC1M77/03K2oBmVJWoFGNUtnVJO4WHKJk3dnPC8HCcj4xBQP1Egm8DWh3Pb3Q==} engines: {node: '>=6.9.0'} @@ -6266,6 +6132,28 @@ packages: regenerator-transform: 0.14.5 dev: true + /@babel/plugin-transform-regenerator/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + regenerator-transform: 0.15.0 + dev: true + + /@babel/plugin-transform-regenerator/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + regenerator-transform: 0.15.0 + dev: false + /@babel/plugin-transform-regenerator/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==} engines: {node: '>=6.9.0'} @@ -6286,26 +6174,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-reserved-words/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-KQzzDnZ9hWQBjwi5lpY5v9shmm6IVG0U9pB18zvMu2i4H90xpT4gmqwPYsn8rObiadYe2M0gmgsiOIF5A/2rtg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-reserved-words/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-KQzzDnZ9hWQBjwi5lpY5v9shmm6IVG0U9pB18zvMu2i4H90xpT4gmqwPYsn8rObiadYe2M0gmgsiOIF5A/2rtg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-reserved-words/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-KQzzDnZ9hWQBjwi5lpY5v9shmm6IVG0U9pB18zvMu2i4H90xpT4gmqwPYsn8rObiadYe2M0gmgsiOIF5A/2rtg==} engines: {node: '>=6.9.0'} @@ -6316,6 +6184,26 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-reserved-words/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-reserved-words/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-reserved-words/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==} engines: {node: '>=6.9.0'} @@ -6402,26 +6290,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-shorthand-properties/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-shorthand-properties/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-shorthand-properties/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg==} engines: {node: '>=6.9.0'} @@ -6432,6 +6300,26 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-shorthand-properties/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-shorthand-properties/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-shorthand-properties/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==} engines: {node: '>=6.9.0'} @@ -6452,28 +6340,6 @@ packages: '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 dev: true - /@babel/plugin-transform-spread/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-+pjJpgAngb53L0iaA5gU/1MLXJIfXcYepLgXB3esVRf4fqmj8f2cxM3/FKaHsZms08hFQJkFccEWuIpm429TXg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 - dev: true - - /@babel/plugin-transform-spread/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-+pjJpgAngb53L0iaA5gU/1MLXJIfXcYepLgXB3esVRf4fqmj8f2cxM3/FKaHsZms08hFQJkFccEWuIpm429TXg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 - dev: false - /@babel/plugin-transform-spread/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-+pjJpgAngb53L0iaA5gU/1MLXJIfXcYepLgXB3esVRf4fqmj8f2cxM3/FKaHsZms08hFQJkFccEWuIpm429TXg==} engines: {node: '>=6.9.0'} @@ -6485,6 +6351,28 @@ packages: '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 dev: true + /@babel/plugin-transform-spread/7.19.0_@babel+core@7.12.9: + resolution: {integrity: sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 + dev: true + + /@babel/plugin-transform-spread/7.19.0_@babel+core@7.16.12: + resolution: {integrity: sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 + dev: false + /@babel/plugin-transform-spread/7.19.0_@babel+core@7.17.8: resolution: {integrity: sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==} engines: {node: '>=6.9.0'} @@ -6505,26 +6393,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-sticky-regex/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-NJa0Bd/87QV5NZZzTuZG5BPJjLYadeSZ9fO6oOUoL4iQx+9EEuw/eEM92SrsT19Yc2jgB1u1hsjqDtH02c3Drw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-sticky-regex/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-NJa0Bd/87QV5NZZzTuZG5BPJjLYadeSZ9fO6oOUoL4iQx+9EEuw/eEM92SrsT19Yc2jgB1u1hsjqDtH02c3Drw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-sticky-regex/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-NJa0Bd/87QV5NZZzTuZG5BPJjLYadeSZ9fO6oOUoL4iQx+9EEuw/eEM92SrsT19Yc2jgB1u1hsjqDtH02c3Drw==} engines: {node: '>=6.9.0'} @@ -6535,6 +6403,26 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-sticky-regex/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-sticky-regex/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-sticky-regex/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==} engines: {node: '>=6.9.0'} @@ -6554,26 +6442,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-template-literals/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-VwbkDDUeenlIjmfNeDX/V0aWrQH2QiVyJtwymVQSzItFDTpxfyJh3EVaQiS0rIN/CqbLGr0VcGmuwyTdZtdIsA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-template-literals/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-VwbkDDUeenlIjmfNeDX/V0aWrQH2QiVyJtwymVQSzItFDTpxfyJh3EVaQiS0rIN/CqbLGr0VcGmuwyTdZtdIsA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-template-literals/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-VwbkDDUeenlIjmfNeDX/V0aWrQH2QiVyJtwymVQSzItFDTpxfyJh3EVaQiS0rIN/CqbLGr0VcGmuwyTdZtdIsA==} engines: {node: '>=6.9.0'} @@ -6584,6 +6452,26 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-template-literals/7.18.9_@babel+core@7.12.9: + resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-template-literals/7.18.9_@babel+core@7.16.12: + resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-template-literals/7.18.9_@babel+core@7.17.8: resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==} engines: {node: '>=6.9.0'} @@ -6603,26 +6491,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-typeof-symbol/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-p2rOixCKRJzpg9JB4gjnG4gjWkWa89ZoYUnl9snJ1cWIcTH/hvxZqfO+WjG6T8DRBpctEol5jw1O5rA8gkCokQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-typeof-symbol/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-p2rOixCKRJzpg9JB4gjnG4gjWkWa89ZoYUnl9snJ1cWIcTH/hvxZqfO+WjG6T8DRBpctEol5jw1O5rA8gkCokQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-typeof-symbol/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-p2rOixCKRJzpg9JB4gjnG4gjWkWa89ZoYUnl9snJ1cWIcTH/hvxZqfO+WjG6T8DRBpctEol5jw1O5rA8gkCokQ==} engines: {node: '>=6.9.0'} @@ -6633,6 +6501,26 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-typeof-symbol/7.18.9_@babel+core@7.12.9: + resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-typeof-symbol/7.18.9_@babel+core@7.16.12: + resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-typeof-symbol/7.18.9_@babel+core@7.17.8: resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==} engines: {node: '>=6.9.0'} @@ -6692,26 +6580,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-unicode-escapes/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-TAV5IGahIz3yZ9/Hfv35TV2xEm+kaBDaZQCn2S/hG9/CZ0DktxJv9eKfPc7yYCvOYR4JGx1h8C+jcSOvgaaI/Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-unicode-escapes/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-TAV5IGahIz3yZ9/Hfv35TV2xEm+kaBDaZQCn2S/hG9/CZ0DktxJv9eKfPc7yYCvOYR4JGx1h8C+jcSOvgaaI/Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-unicode-escapes/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-TAV5IGahIz3yZ9/Hfv35TV2xEm+kaBDaZQCn2S/hG9/CZ0DktxJv9eKfPc7yYCvOYR4JGx1h8C+jcSOvgaaI/Q==} engines: {node: '>=6.9.0'} @@ -6722,6 +6590,26 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-unicode-escapes/7.18.10_@babel+core@7.12.9: + resolution: {integrity: sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-unicode-escapes/7.18.10_@babel+core@7.16.12: + resolution: {integrity: sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-unicode-escapes/7.18.10_@babel+core@7.17.8: resolution: {integrity: sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==} engines: {node: '>=6.9.0'} @@ -6742,28 +6630,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-transform-unicode-regex/7.16.7_@babel+core@7.12.9: - resolution: {integrity: sha512-oC5tYYKw56HO75KZVLQ+R/Nl3Hro9kf8iG0hXoaHP7tjAyCpvqBiSNe6vGrZni1Z6MggmUOC6A7VP7AVmw225Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - - /@babel/plugin-transform-unicode-regex/7.16.7_@babel+core@7.16.12: - resolution: {integrity: sha512-oC5tYYKw56HO75KZVLQ+R/Nl3Hro9kf8iG0hXoaHP7tjAyCpvqBiSNe6vGrZni1Z6MggmUOC6A7VP7AVmw225Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.16.12 - '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - dev: false - /@babel/plugin-transform-unicode-regex/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-oC5tYYKw56HO75KZVLQ+R/Nl3Hro9kf8iG0hXoaHP7tjAyCpvqBiSNe6vGrZni1Z6MggmUOC6A7VP7AVmw225Q==} engines: {node: '>=6.9.0'} @@ -6775,6 +6641,28 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-unicode-regex/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-unicode-regex/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-unicode-regex/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==} engines: {node: '>=6.9.0'} @@ -6875,28 +6763,28 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.17.7 + '@babel/compat-data': 7.19.3 '@babel/core': 7.12.9 '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 - '@babel/helper-validator-option': 7.16.7 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-proposal-async-generator-functions': 7.16.8_@babel+core@7.12.9 - '@babel/plugin-proposal-class-properties': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-proposal-class-static-block': 7.17.6_@babel+core@7.12.9 - '@babel/plugin-proposal-dynamic-import': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-proposal-export-namespace-from': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-proposal-json-strings': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-proposal-logical-assignment-operators': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-proposal-nullish-coalescing-operator': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-proposal-numeric-separator': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-proposal-object-rest-spread': 7.17.3_@babel+core@7.12.9 - '@babel/plugin-proposal-optional-catch-binding': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-proposal-optional-chaining': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-proposal-private-methods': 7.16.11_@babel+core@7.12.9 - '@babel/plugin-proposal-private-property-in-object': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-proposal-unicode-property-regex': 7.16.7_@babel+core@7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-validator-option': 7.18.6 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.18.9_@babel+core@7.12.9 + '@babel/plugin-proposal-async-generator-functions': 7.19.1_@babel+core@7.12.9 + '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-proposal-class-static-block': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-proposal-dynamic-import': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-proposal-export-namespace-from': 7.18.9_@babel+core@7.12.9 + '@babel/plugin-proposal-json-strings': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-proposal-logical-assignment-operators': 7.18.9_@babel+core@7.12.9 + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-proposal-numeric-separator': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-proposal-object-rest-spread': 7.18.9_@babel+core@7.12.9 + '@babel/plugin-proposal-optional-catch-binding': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.12.9 + '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-proposal-private-property-in-object': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.12.9 '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.12.9 '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.12.9 '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.12.9 @@ -6911,44 +6799,44 @@ packages: '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.12.9 '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.12.9 '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.12.9 - '@babel/plugin-transform-arrow-functions': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-async-to-generator': 7.16.8_@babel+core@7.12.9 - '@babel/plugin-transform-block-scoped-functions': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-block-scoping': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-classes': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-computed-properties': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-destructuring': 7.17.7_@babel+core@7.12.9 - '@babel/plugin-transform-dotall-regex': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-duplicate-keys': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-exponentiation-operator': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-for-of': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-function-name': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-literals': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-member-expression-literals': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-modules-amd': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-modules-commonjs': 7.17.7_@babel+core@7.12.9 - '@babel/plugin-transform-modules-systemjs': 7.17.8_@babel+core@7.12.9 - '@babel/plugin-transform-modules-umd': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-named-capturing-groups-regex': 7.16.8_@babel+core@7.12.9 - '@babel/plugin-transform-new-target': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-object-super': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-parameters': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-property-literals': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-regenerator': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-reserved-words': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-shorthand-properties': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-spread': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-sticky-regex': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-template-literals': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-typeof-symbol': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-unicode-escapes': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-unicode-regex': 7.16.7_@babel+core@7.12.9 + '@babel/plugin-transform-arrow-functions': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-transform-async-to-generator': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-transform-block-scoped-functions': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-transform-block-scoping': 7.18.9_@babel+core@7.12.9 + '@babel/plugin-transform-classes': 7.19.0_@babel+core@7.12.9 + '@babel/plugin-transform-computed-properties': 7.18.9_@babel+core@7.12.9 + '@babel/plugin-transform-destructuring': 7.18.13_@babel+core@7.12.9 + '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-transform-duplicate-keys': 7.18.9_@babel+core@7.12.9 + '@babel/plugin-transform-exponentiation-operator': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-transform-for-of': 7.18.8_@babel+core@7.12.9 + '@babel/plugin-transform-function-name': 7.18.9_@babel+core@7.12.9 + '@babel/plugin-transform-literals': 7.18.9_@babel+core@7.12.9 + '@babel/plugin-transform-member-expression-literals': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-transform-modules-amd': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-transform-modules-commonjs': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-transform-modules-systemjs': 7.19.0_@babel+core@7.12.9 + '@babel/plugin-transform-modules-umd': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-transform-named-capturing-groups-regex': 7.19.1_@babel+core@7.12.9 + '@babel/plugin-transform-new-target': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-transform-object-super': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.12.9 + '@babel/plugin-transform-property-literals': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-transform-regenerator': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-transform-reserved-words': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-transform-shorthand-properties': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-transform-spread': 7.19.0_@babel+core@7.12.9 + '@babel/plugin-transform-sticky-regex': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.12.9 + '@babel/plugin-transform-typeof-symbol': 7.18.9_@babel+core@7.12.9 + '@babel/plugin-transform-unicode-escapes': 7.18.10_@babel+core@7.12.9 + '@babel/plugin-transform-unicode-regex': 7.18.6_@babel+core@7.12.9 '@babel/preset-modules': 0.1.5_@babel+core@7.12.9 '@babel/types': 7.19.3 - babel-plugin-polyfill-corejs2: 0.3.0_@babel+core@7.12.9 + babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.12.9 babel-plugin-polyfill-corejs3: 0.5.2_@babel+core@7.12.9 babel-plugin-polyfill-regenerator: 0.3.0_@babel+core@7.12.9 - core-js-compat: 3.21.1 + core-js-compat: 3.25.5 semver: 6.3.0 transitivePeerDependencies: - supports-color @@ -6960,28 +6848,28 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.17.7 + '@babel/compat-data': 7.19.3 '@babel/core': 7.16.12 '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.18.9 - '@babel/helper-validator-option': 7.16.7 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-proposal-async-generator-functions': 7.16.8_@babel+core@7.16.12 - '@babel/plugin-proposal-class-properties': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-proposal-class-static-block': 7.17.6_@babel+core@7.16.12 - '@babel/plugin-proposal-dynamic-import': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-proposal-export-namespace-from': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-proposal-json-strings': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-proposal-logical-assignment-operators': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-proposal-nullish-coalescing-operator': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-proposal-numeric-separator': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-proposal-object-rest-spread': 7.17.3_@babel+core@7.16.12 - '@babel/plugin-proposal-optional-catch-binding': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-proposal-optional-chaining': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-proposal-private-methods': 7.16.11_@babel+core@7.16.12 - '@babel/plugin-proposal-private-property-in-object': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-proposal-unicode-property-regex': 7.16.7_@babel+core@7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-validator-option': 7.18.6 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.18.9_@babel+core@7.16.12 + '@babel/plugin-proposal-async-generator-functions': 7.19.1_@babel+core@7.16.12 + '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-proposal-class-static-block': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-proposal-dynamic-import': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-proposal-export-namespace-from': 7.18.9_@babel+core@7.16.12 + '@babel/plugin-proposal-json-strings': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-proposal-logical-assignment-operators': 7.18.9_@babel+core@7.16.12 + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-proposal-numeric-separator': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-proposal-object-rest-spread': 7.18.9_@babel+core@7.16.12 + '@babel/plugin-proposal-optional-catch-binding': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.16.12 + '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-proposal-private-property-in-object': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.16.12 '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.16.12 '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.16.12 '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.16.12 @@ -6996,44 +6884,44 @@ packages: '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.16.12 '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.16.12 '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.16.12 - '@babel/plugin-transform-arrow-functions': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-async-to-generator': 7.16.8_@babel+core@7.16.12 - '@babel/plugin-transform-block-scoped-functions': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-block-scoping': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-classes': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-computed-properties': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-destructuring': 7.17.7_@babel+core@7.16.12 - '@babel/plugin-transform-dotall-regex': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-duplicate-keys': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-exponentiation-operator': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-for-of': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-function-name': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-literals': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-member-expression-literals': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-modules-amd': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-modules-commonjs': 7.17.7_@babel+core@7.16.12 - '@babel/plugin-transform-modules-systemjs': 7.17.8_@babel+core@7.16.12 - '@babel/plugin-transform-modules-umd': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-named-capturing-groups-regex': 7.16.8_@babel+core@7.16.12 - '@babel/plugin-transform-new-target': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-object-super': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-parameters': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-property-literals': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-regenerator': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-reserved-words': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-shorthand-properties': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-spread': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-sticky-regex': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-template-literals': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-typeof-symbol': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-unicode-escapes': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-unicode-regex': 7.16.7_@babel+core@7.16.12 + '@babel/plugin-transform-arrow-functions': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-transform-async-to-generator': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-transform-block-scoped-functions': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-transform-block-scoping': 7.18.9_@babel+core@7.16.12 + '@babel/plugin-transform-classes': 7.19.0_@babel+core@7.16.12 + '@babel/plugin-transform-computed-properties': 7.18.9_@babel+core@7.16.12 + '@babel/plugin-transform-destructuring': 7.18.13_@babel+core@7.16.12 + '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-transform-duplicate-keys': 7.18.9_@babel+core@7.16.12 + '@babel/plugin-transform-exponentiation-operator': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-transform-for-of': 7.18.8_@babel+core@7.16.12 + '@babel/plugin-transform-function-name': 7.18.9_@babel+core@7.16.12 + '@babel/plugin-transform-literals': 7.18.9_@babel+core@7.16.12 + '@babel/plugin-transform-member-expression-literals': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-transform-modules-amd': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-transform-modules-commonjs': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-transform-modules-systemjs': 7.19.0_@babel+core@7.16.12 + '@babel/plugin-transform-modules-umd': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-transform-named-capturing-groups-regex': 7.19.1_@babel+core@7.16.12 + '@babel/plugin-transform-new-target': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-transform-object-super': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.16.12 + '@babel/plugin-transform-property-literals': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-transform-regenerator': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-transform-reserved-words': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-transform-shorthand-properties': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-transform-spread': 7.19.0_@babel+core@7.16.12 + '@babel/plugin-transform-sticky-regex': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.16.12 + '@babel/plugin-transform-typeof-symbol': 7.18.9_@babel+core@7.16.12 + '@babel/plugin-transform-unicode-escapes': 7.18.10_@babel+core@7.16.12 + '@babel/plugin-transform-unicode-regex': 7.18.6_@babel+core@7.16.12 '@babel/preset-modules': 0.1.5_@babel+core@7.16.12 '@babel/types': 7.19.3 - babel-plugin-polyfill-corejs2: 0.3.0_@babel+core@7.16.12 + babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.16.12 babel-plugin-polyfill-corejs3: 0.5.2_@babel+core@7.16.12 babel-plugin-polyfill-regenerator: 0.3.0_@babel+core@7.16.12 - core-js-compat: 3.21.1 + core-js-compat: 3.25.5 semver: 6.3.0 transitivePeerDependencies: - supports-color @@ -7414,6 +7302,7 @@ packages: dependencies: '@babel/helper-validator-identifier': 7.19.1 to-fast-properties: 2.0.0 + dev: true /@babel/types/7.17.0: resolution: {integrity: sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==} @@ -9591,7 +9480,7 @@ packages: '@react-native-community/cli-tools': 9.1.0 '@react-native-community/cli-types': 9.1.0 chalk: 4.1.2 - commander: 9.4.1 + commander: 9.4.0 execa: 1.0.0 find-up: 4.1.0 fs-extra: 8.1.0 @@ -14657,7 +14546,7 @@ packages: '@wordpress/browserslist-config': 4.1.3 '@wordpress/element': 4.20.0 '@wordpress/warning': 2.19.0 - browserslist: 4.21.4 + browserslist: 4.19.3 core-js: 3.25.5 transitivePeerDependencies: - supports-color @@ -14717,7 +14606,7 @@ packages: '@wordpress/browserslist-config': 5.7.0 '@wordpress/element': 4.20.0 '@wordpress/warning': 2.19.0 - browserslist: 4.21.4 + browserslist: 4.19.3 core-js: 3.25.5 transitivePeerDependencies: - supports-color @@ -18478,19 +18367,6 @@ packages: - supports-color dev: true - /babel-plugin-polyfill-corejs2/0.3.0_@babel+core@7.16.12: - resolution: {integrity: sha512-wMDoBJ6uG4u4PNFh72Ty6t3EgfA91puCuAwKIazbQlci+ENb/UU9A3xG5lutjUIiXCIn1CY5L15r9LimiJyrSA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.19.3 - '@babel/core': 7.16.12 - '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.16.12 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: false - /babel-plugin-polyfill-corejs2/0.3.0_@babel+core@7.17.8: resolution: {integrity: sha512-wMDoBJ6uG4u4PNFh72Ty6t3EgfA91puCuAwKIazbQlci+ENb/UU9A3xG5lutjUIiXCIn1CY5L15r9LimiJyrSA==} peerDependencies: @@ -18504,6 +18380,19 @@ packages: - supports-color dev: true + /babel-plugin-polyfill-corejs2/0.3.3_@babel+core@7.12.9: + resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.19.3 + '@babel/core': 7.12.9 + '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.12.9 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + /babel-plugin-polyfill-corejs2/0.3.3_@babel+core@7.16.12: resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} peerDependencies: @@ -19594,7 +19483,7 @@ packages: /caniuse-api/3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} dependencies: - browserslist: 4.21.4 + browserslist: 4.19.3 caniuse-lite: 1.0.30001418 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 @@ -20349,11 +20238,11 @@ packages: /commander/9.4.0: resolution: {integrity: sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==} engines: {node: ^12.20.0 || >=14} - dev: false /commander/9.4.1: resolution: {integrity: sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==} engines: {node: ^12.20.0 || >=14} + dev: true /comment-parser/0.7.6: resolution: {integrity: sha512-GKNxVA7/iuTnAqGADlTWX4tkhzxZKXp5fLJqKTlQLHkE65XDUKutZ3BHaJC5IGcper2tT3QRD1xr4o3jNpgXXg==} @@ -20452,6 +20341,7 @@ packages: engines: {node: '>= 6.0.0'} dependencies: json5: 2.2.0 + dev: false /config/3.3.7: resolution: {integrity: sha512-mX/n7GKDYZMqvvkY6e6oBY49W8wxdmQt+ho/5lhwFDXqQW9gI+Ahp8EKp8VAbISPnmf2+Bv5uZK7lKXZ6pf1aA==} @@ -20596,8 +20486,9 @@ packages: /core-js-compat/3.21.1: resolution: {integrity: sha512-gbgX5AUvMb8gwxC7FLVWYT7Kkgu/y7+h/h1X43yJkNqhlK2fuYyQimqvKGNZFAY6CKii/GFKJ2cp/1/42TN36g==} dependencies: - browserslist: 4.21.4 + browserslist: 4.19.3 semver: 7.0.0 + dev: true /core-js-compat/3.25.5: resolution: {integrity: sha512-ovcyhs2DEBUIE0MGEKHP4olCUW/XYte3Vroyxuh38rD1wAO4dHohsovUC4eAOuzFxE6b+RXvBU3UZ9o0YhUTkA==} @@ -22534,11 +22425,11 @@ packages: eslint-import-resolver-node: 0.3.6 eslint-module-utils: 2.7.3_lkzaig2qiyp6elizstfbgvzhie has: 1.0.3 - is-core-module: 2.10.0 + is-core-module: 2.8.0 is-glob: 4.0.3 - minimatch: 3.1.2 + minimatch: 3.0.4 object.values: 1.1.5 - resolve: 1.22.1 + resolve: 1.20.0 tsconfig-paths: 3.14.0 transitivePeerDependencies: - eslint-import-resolver-typescript @@ -22565,11 +22456,11 @@ packages: eslint-import-resolver-node: 0.3.6 eslint-module-utils: 2.7.3_fmuy6wfytpxcy4lufnxcokvnry has: 1.0.3 - is-core-module: 2.10.0 + is-core-module: 2.8.0 is-glob: 4.0.3 - minimatch: 3.1.2 + minimatch: 3.0.4 object.values: 1.1.5 - resolve: 1.22.1 + resolve: 1.20.0 tsconfig-paths: 3.14.0 transitivePeerDependencies: - eslint-import-resolver-typescript @@ -31033,7 +30924,6 @@ packages: resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==} dependencies: brace-expansion: 1.1.11 - dev: true /minimatch/3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -32813,7 +32703,7 @@ packages: resolution: {integrity: sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==} engines: {node: '>=6.9.0'} dependencies: - browserslist: 4.21.4 + browserslist: 4.19.3 color: 3.2.1 has: 1.0.3 postcss: 7.0.39 @@ -32825,7 +32715,7 @@ packages: peerDependencies: postcss: ^8.2.15 dependencies: - browserslist: 4.21.4 + browserslist: 4.19.3 caniuse-api: 3.0.0 colord: 2.9.2 postcss: 8.4.12 @@ -33015,7 +32905,7 @@ packages: resolution: {integrity: sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==} engines: {node: '>=6.9.0'} dependencies: - browserslist: 4.21.4 + browserslist: 4.19.3 caniuse-api: 3.0.0 cssnano-util-same-parent: 4.0.1 postcss: 7.0.39 @@ -33028,7 +32918,7 @@ packages: peerDependencies: postcss: ^8.2.15 dependencies: - browserslist: 4.21.4 + browserslist: 4.19.3 caniuse-api: 3.0.0 cssnano-utils: 3.1.0_postcss@8.4.12 postcss: 8.4.12 @@ -33082,7 +32972,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: alphanum-sort: 1.0.2 - browserslist: 4.21.4 + browserslist: 4.19.3 cssnano-util-get-arguments: 4.0.0 postcss: 7.0.39 postcss-value-parser: 3.3.1 @@ -33094,7 +32984,7 @@ packages: peerDependencies: postcss: ^8.2.15 dependencies: - browserslist: 4.21.4 + browserslist: 4.19.3 cssnano-utils: 3.1.0_postcss@8.4.12 postcss: 8.4.12 postcss-value-parser: 4.2.0 @@ -33299,7 +33189,7 @@ packages: resolution: {integrity: sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==} engines: {node: '>=6.9.0'} dependencies: - browserslist: 4.21.4 + browserslist: 4.19.3 postcss: 7.0.39 postcss-value-parser: 3.3.1 @@ -33309,7 +33199,7 @@ packages: peerDependencies: postcss: ^8.2.15 dependencies: - browserslist: 4.21.4 + browserslist: 4.19.3 postcss: 8.4.12 postcss-value-parser: 4.2.0 dev: true @@ -33374,7 +33264,7 @@ packages: resolution: {integrity: sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==} engines: {node: '>=6.9.0'} dependencies: - browserslist: 4.21.4 + browserslist: 4.19.3 caniuse-api: 3.0.0 has: 1.0.3 postcss: 7.0.39 @@ -33385,7 +33275,7 @@ packages: peerDependencies: postcss: ^8.2.15 dependencies: - browserslist: 4.21.4 + browserslist: 4.19.3 caniuse-api: 3.0.0 postcss: 8.4.12 dev: true @@ -35499,6 +35389,7 @@ packages: resolution: {integrity: sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==} dependencies: '@babel/runtime': 7.19.0 + dev: true /regenerator-transform/0.15.0: resolution: {integrity: sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==} @@ -36305,6 +36196,7 @@ packages: /semver/7.0.0: resolution: {integrity: sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==} hasBin: true + dev: true /semver/7.3.5: resolution: {integrity: sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==} @@ -37282,7 +37174,7 @@ packages: resolution: {integrity: sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==} engines: {node: '>=6.9.0'} dependencies: - browserslist: 4.21.4 + browserslist: 4.19.3 postcss: 7.0.39 postcss-selector-parser: 3.1.2 @@ -37292,7 +37184,7 @@ packages: peerDependencies: postcss: ^8.2.15 dependencies: - browserslist: 4.21.4 + browserslist: 4.19.3 postcss: 8.4.12 postcss-selector-parser: 6.0.9 dev: true @@ -38044,7 +37936,7 @@ packages: serialize-javascript: 6.0.0 source-map: 0.6.1 terser: 5.10.0_acorn@8.8.1 - webpack: 5.70.0 + webpack: 5.70.0_webpack-cli@3.3.12 transitivePeerDependencies: - acorn From e4303a7b25af58fc3c6c73f03cc60022236f2b5a Mon Sep 17 00:00:00 2001 From: Sam Seay Date: Tue, 14 Feb 2023 18:34:47 +1300 Subject: [PATCH 176/343] Add smooth generator to the beta tester userscript (#36676) --- .../changelog/dev-add-smooth-generator-user-script | 4 ++++ .../userscripts/wc-live-branches.user.js | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 plugins/woocommerce-beta-tester/changelog/dev-add-smooth-generator-user-script diff --git a/plugins/woocommerce-beta-tester/changelog/dev-add-smooth-generator-user-script b/plugins/woocommerce-beta-tester/changelog/dev-add-smooth-generator-user-script new file mode 100644 index 00000000000..27b8b23adb8 --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/dev-add-smooth-generator-user-script @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add userscript option to install wc smooth generator. diff --git a/plugins/woocommerce-beta-tester/userscripts/wc-live-branches.user.js b/plugins/woocommerce-beta-tester/userscripts/wc-live-branches.user.js index 42efdb730b3..0485708de14 100644 --- a/plugins/woocommerce-beta-tester/userscripts/wc-live-branches.user.js +++ b/plugins/woocommerce-beta-tester/userscripts/wc-live-branches.user.js @@ -141,6 +141,10 @@

Install additional plugins

${ getOptionsList( [ + { + label: 'WooCommerce Smooth Generator', + name: 'wc-smooth-generator', + }, { label: 'Jetpack', name: 'nojetpack', From c4440b9586e89411476974a3b3d9b6a4a1dedca4 Mon Sep 17 00:00:00 2001 From: Nestor Soriano Date: Tue, 14 Feb 2023 09:56:48 +0100 Subject: [PATCH 177/343] Don't assume the character encoding parameter is present --- .../includes/import/class-wc-product-csv-importer.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 6a32aaaf9c9..12f6a390a2f 100644 --- a/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php +++ b/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php @@ -6,6 +6,8 @@ * @version 3.1.0 */ +use Automattic\WooCommerce\Utilities\ArrayUtil; + if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -90,7 +92,7 @@ class WC_Product_CSV_Importer extends WC_Product_Importer { if ( false !== $handle ) { $this->raw_keys = array_map( 'trim', fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ) ); // @codingStandardsIgnoreLine - if ( $this->params['character_encoding'] ) { + if ( ArrayUtil::is_truthy( $this->params, 'character_encoding' ) ) { $this->raw_keys = array_map( array( $this, 'adjust_character_encoding' ), $this->raw_keys ); } @@ -110,7 +112,7 @@ class WC_Product_CSV_Importer extends WC_Product_Importer { $row = fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ); // @codingStandardsIgnoreLine if ( false !== $row ) { - if ( $this->params['character_encoding'] ) { + if ( ArrayUtil::is_truthy( $this->params, 'character_encoding' ) ) { $row = array_map( array( $this, 'adjust_character_encoding' ), $row ); } From 55af1d136552aad739a7803cfb8937553e84ec03 Mon Sep 17 00:00:00 2001 From: Atanas Penchev Date: Tue, 14 Feb 2023 13:08:07 +0200 Subject: [PATCH 178/343] FlexSlider legacy browser code clean-up (#36690) --- .../changelog/flexslider-legacy-cleanup | 4 + .../legacy/js/flexslider/jquery.flexslider.js | 178 ++---------------- 2 files changed, 17 insertions(+), 165 deletions(-) create mode 100644 plugins/woocommerce/changelog/flexslider-legacy-cleanup diff --git a/plugins/woocommerce/changelog/flexslider-legacy-cleanup b/plugins/woocommerce/changelog/flexslider-legacy-cleanup new file mode 100644 index 00000000000..5b242572b85 --- /dev/null +++ b/plugins/woocommerce/changelog/flexslider-legacy-cleanup @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Delete FlexSlider code for legacy browsers. diff --git a/plugins/woocommerce/client/legacy/js/flexslider/jquery.flexslider.js b/plugins/woocommerce/client/legacy/js/flexslider/jquery.flexslider.js index a7420f1de2b..852a326f63f 100755 --- a/plugins/woocommerce/client/legacy/js/flexslider/jquery.flexslider.js +++ b/plugins/woocommerce/client/legacy/js/flexslider/jquery.flexslider.js @@ -21,10 +21,9 @@ slider.vars = $.extend({}, $.flexslider.defaults, options); var namespace = slider.vars.namespace, - msGesture = window.navigator && window.navigator.msPointerEnabled && window.MSGesture, - touch = (( "ontouchstart" in window ) || msGesture || window.DocumentTouch && document instanceof DocumentTouch) && slider.vars.touch, + touch = (( "ontouchstart" in window ) || window.DocumentTouch && document instanceof DocumentTouch) && slider.vars.touch, // deprecating this idea, as devices are being released with both of these events - eventType = "click touchend MSPointerUp keyup", + eventType = "click touchend keyup", watchedEvent = "", watchedEventClearTimer, vertical = slider.vars.direction === "vertical", @@ -63,18 +62,8 @@ slider.started = false; slider.startTimeout = null; // TOUCH/USECSS: - slider.transitions = !slider.vars.video && !fade && slider.vars.useCSS && (function() { - var obj = document.createElement('div'), - props = ['perspectiveProperty', 'WebkitPerspective', 'MozPerspective', 'OPerspective', 'msPerspective']; - for (var i in props) { - if ( obj.style[ props[i] ] !== undefined ) { - slider.pfx = props[i].replace('Perspective','').toLowerCase(); - slider.prop = "-" + slider.pfx + "-transform"; - return true; - } - } - return false; - }()); + slider.transitions = !slider.vars.video && !fade && slider.vars.useCSS; + if (slider.transitions) slider.prop = "transform"; slider.isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; slider.ensureAnimationEnd = ''; // CONTROLSCONTAINER: @@ -132,7 +121,7 @@ if (slider.vars.pausePlay) { methods.pausePlay.setup(); } //PAUSE WHEN INVISIBLE - if (slider.vars.slideshow && slider.vars.pauseInvisible) { methods.pauseInvisible.init(); } + if (slider.vars.slideshow && slider.vars.pauseInvisible) { methods.pauseInvisible(); } // SLIDSESHOW if (slider.vars.slideshow) { @@ -145,7 +134,7 @@ } // initialize animation //If we're visible, or we don't use PageVisibility API - if(!slider.vars.pauseInvisible || !methods.pauseInvisible.isHidden()) { + if(!slider.vars.pauseInvisible || document.visibilityState === 'visible') { (slider.vars.initDelay > 0) ? slider.startTimeout = setTimeout(slider.play, slider.vars.initDelay) : slider.play(); } } @@ -172,7 +161,6 @@ slider.animatingTo = Math.floor(slider.currentSlide/slider.move); slider.currentItem = slider.currentSlide; slider.slides.removeClass(namespace + "active-slide").eq(slider.currentItem).addClass(namespace + "active-slide"); - if(!msGesture){ slider.slides.on(eventType, function(e){ e.preventDefault(); var $slide = $(this), @@ -192,29 +180,6 @@ slider.flexAnimate(target, slider.vars.pauseOnAction, false, true, true); } }); - }else{ - el._slider = slider; - slider.slides.each(function (){ - var that = this; - that._gesture = new MSGesture(); - that._gesture.target = that; - that.addEventListener("MSPointerDown", function (e){ - e.preventDefault(); - if(e.currentTarget._gesture) { - e.currentTarget._gesture.addPointer(e.pointerId); - } - }, false); - that.addEventListener("MSGestureTap", function (e){ - e.preventDefault(); - var $slide = $(this), - target = $slide.index(); - if (!$(slider.vars.asNavFor).data('flexslider').animating && !$slide.hasClass('active')) { - slider.direction = (slider.currentItem < target) ? "next" : "prev"; - slider.flexAnimate(target, slider.vars.pauseOnAction, false, true, true); - } - }); - }); - } } }, controlNav: { @@ -449,11 +414,10 @@ localY = 0, accDx = 0; - if(!msGesture){ onTouchStart = function(e) { if (slider.animating) { e.preventDefault(); - } else if ( ( window.navigator.msPointerEnabled ) || e.touches.length === 1 ) { + } else if ( e.touches.length === 1 ) { slider.pause(); // CAROUSEL: cwidth = (vertical) ? slider.h : slider. w; @@ -520,92 +484,6 @@ }; el.addEventListener('touchstart', onTouchStart, false); - }else{ - el.style.msTouchAction = "none"; - el._gesture = new MSGesture(); - el._gesture.target = el; - el.addEventListener("MSPointerDown", onMSPointerDown, false); - el._slider = slider; - el.addEventListener("MSGestureChange", onMSGestureChange, false); - el.addEventListener("MSGestureEnd", onMSGestureEnd, false); - - function onMSPointerDown(e){ - e.stopPropagation(); - if (slider.animating) { - e.preventDefault(); - }else{ - slider.pause(); - el._gesture.addPointer(e.pointerId); - accDx = 0; - cwidth = (vertical) ? slider.h : slider. w; - startT = Number(new Date()); - // CAROUSEL: - - offset = (carousel && reverse && slider.animatingTo === slider.last) ? 0 : - (carousel && reverse) ? slider.limit - (((slider.itemW + slider.vars.itemMargin) * slider.move) * slider.animatingTo) : - (carousel && slider.currentSlide === slider.last) ? slider.limit : - (carousel) ? ((slider.itemW + slider.vars.itemMargin) * slider.move) * slider.currentSlide : - (reverse) ? (slider.last - slider.currentSlide + slider.cloneOffset) * cwidth : (slider.currentSlide + slider.cloneOffset) * cwidth; - } - } - - function onMSGestureChange(e) { - e.stopPropagation(); - var slider = e.target._slider; - if(!slider){ - return; - } - var transX = -e.translationX, - transY = -e.translationY; - - //Accumulate translations. - accDx = accDx + ((vertical) ? transY : transX); - dx = (slider.vars.rtl?-1:1)*accDx; - scrolling = (vertical) ? (Math.abs(accDx) < Math.abs(-transX)) : (Math.abs(accDx) < Math.abs(-transY)); - - if(e.detail === e.MSGESTURE_FLAG_INERTIA){ - setImmediate(function (){ - el._gesture.stop(); - }); - - return; - } - - if (!scrolling || Number(new Date()) - startT > 500) { - e.preventDefault(); - if (!fade && slider.transitions) { - if (!slider.vars.animationLoop) { - dx = accDx / ((slider.currentSlide === 0 && accDx < 0 || slider.currentSlide === slider.last && accDx > 0) ? (Math.abs(accDx) / cwidth + 2) : 1); - } - slider.setProps(offset + dx, "setTouch"); - } - } - } - - function onMSGestureEnd(e) { - e.stopPropagation(); - var slider = e.target._slider; - if(!slider){ - return; - } - if (slider.animatingTo === slider.currentSlide && !scrolling && !(dx === null)) { - var updateDx = (reverse) ? -dx : dx, - target = (updateDx > 0) ? slider.getTarget('next') : slider.getTarget('prev'); - - if (slider.canAdvance(target) && (Number(new Date()) - startT < 550 && Math.abs(updateDx) > 50 || Math.abs(updateDx) > cwidth/2)) { - slider.flexAnimate(target, slider.vars.pauseOnAction); - } else { - if (!fade) { slider.flexAnimate(slider.currentSlide, slider.vars.pauseOnAction, true); } - } - } - - startX = null; - startY = null; - dx = null; - offset = null; - accDx = 0; - } - } }, resize: function() { if (!slider.animating && slider.is(':visible')) { @@ -654,14 +532,9 @@ }); return $clone; }, - pauseInvisible: { - visProp: null, - init: function() { - var visProp = methods.pauseInvisible.getHiddenProp(); - if (visProp) { - var evtname = visProp.replace(/[H|h]idden/,'') + 'visibilitychange'; - document.addEventListener(evtname, function() { - if (methods.pauseInvisible.isHidden()) { + pauseInvisible: function() { + document.addEventListener('visibilitychange', function() { + if (document.visibilityState === 'hidden') { if(slider.startTimeout) { clearTimeout(slider.startTimeout); //If clock is ticking, stop timer and prevent from starting while invisible } else { @@ -680,30 +553,6 @@ } } }); - } - }, - isHidden: function() { - var prop = methods.pauseInvisible.getHiddenProp(); - if (!prop) { - return false; - } - return document[prop]; - }, - getHiddenProp: function() { - var prefixes = ['webkit','moz','ms','o']; - // if 'hidden' is natively supported just return it - if ('hidden' in document) { - return 'hidden'; - } - // otherwise loop over all the known prefixes until we find one - for ( var i = 0; i < prefixes.length; i++ ) { - if ((prefixes[i] + 'Hidden') in document) { - return prefixes[i] + 'Hidden'; - } - } - // otherwise it's not supported - return null; - } }, setToClearWatchedEvent: function() { clearTimeout(watchedEventClearTimer); @@ -798,8 +647,8 @@ } // Unbind previous transitionEnd events and re-bind new transitionEnd event - slider.container.off("webkitTransitionEnd transitionend"); - slider.container.on("webkitTransitionEnd transitionend", function() { + slider.container.off("transitionend"); + slider.container.on("transitionend", function() { clearTimeout(slider.ensureAnimationEnd); slider.wrapup(dimension); }); @@ -921,7 +770,6 @@ if (slider.transitions) { target = (vertical) ? "translate3d(0," + target + ",0)" : "translate3d(" + (parseInt(target)+'px') + ",0,0)"; dur = (dur !== undefined) ? (dur/1000) + "s" : "0s"; - slider.container.css("-" + slider.pfx + "-transition-duration", dur); slider.container.css("transition-duration", dur); } @@ -1000,7 +848,7 @@ slider.slides.css({ "opacity": 0, "display": "block", "zIndex": 1 }).eq(slider.currentSlide).css({"zIndex": 2}).animate({"opacity": 1},slider.vars.animationSpeed,slider.vars.easing); } } else { - slider.slides.css({ "opacity": 0, "display": "block", "webkitTransition": "opacity " + slider.vars.animationSpeed / 1000 + "s ease", "zIndex": 1 }).eq(slider.currentSlide).css({ "opacity": 1, "zIndex": 2}); + slider.slides.css({ "opacity": 0, "display": "block", "transition": "opacity " + slider.vars.animationSpeed / 1000 + "s ease", "zIndex": 1 }).eq(slider.currentSlide).css({ "opacity": 1, "zIndex": 2}); } } // SMOOTH HEIGHT: From 993f1f487995ad39dab81ffc72c83abcf7e1cfad Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Wed, 15 Feb 2023 13:12:21 +0100 Subject: [PATCH 179/343] Fix SelectControl and TreeControl styles (#36718) * Change TreeItem background hover/focus color to $gray-100 * Remove errand importing of native colors * Changelog --- packages/js/components/changelog/fix-select-control-style | 4 ++++ .../components/src/experimental-tree-control/tree-item.scss | 2 +- packages/js/components/src/style.scss | 1 - 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 packages/js/components/changelog/fix-select-control-style diff --git a/packages/js/components/changelog/fix-select-control-style b/packages/js/components/changelog/fix-select-control-style new file mode 100644 index 00000000000..256abe194ed --- /dev/null +++ b/packages/js/components/changelog/fix-select-control-style @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix SelectControl and TreeControl styles. diff --git a/packages/js/components/src/experimental-tree-control/tree-item.scss b/packages/js/components/src/experimental-tree-control/tree-item.scss index 72579e8757d..4c36ce5ab6f 100644 --- a/packages/js/components/src/experimental-tree-control/tree-item.scss +++ b/packages/js/components/src/experimental-tree-control/tree-item.scss @@ -17,7 +17,7 @@ &:hover, &:focus-within { - background-color: $gray-0; + background-color: $gray-100; } } &__label { diff --git a/packages/js/components/src/style.scss b/packages/js/components/src/style.scss index a38163a2a32..be220285b73 100644 --- a/packages/js/components/src/style.scss +++ b/packages/js/components/src/style.scss @@ -1,7 +1,6 @@ /** * External Dependencies */ -@import 'node_modules/@wordpress/base-styles/colors.native'; @import '@automattic/tour-kit/dist/esm/styles.scss'; /** From 2efe0a0f6a7c944164fb4d4b04f63bb6a647219b Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Wed, 15 Feb 2023 16:36:56 +0100 Subject: [PATCH 180/343] add tracking for local pickup --- .../changelog/add-tracking-for-loca-pickup | 4 +++ .../woocommerce/includes/class-wc-tracker.php | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-tracking-for-loca-pickup diff --git a/plugins/woocommerce/changelog/add-tracking-for-loca-pickup b/plugins/woocommerce/changelog/add-tracking-for-loca-pickup new file mode 100644 index 00000000000..20ccbdbeb1f --- /dev/null +++ b/plugins/woocommerce/changelog/add-tracking-for-loca-pickup @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Add tracking for local pickup method in Checkout diff --git a/plugins/woocommerce/includes/class-wc-tracker.php b/plugins/woocommerce/includes/class-wc-tracker.php index abad47fe2e8..15b99f056b7 100644 --- a/plugins/woocommerce/includes/class-wc-tracker.php +++ b/plugins/woocommerce/includes/class-wc-tracker.php @@ -790,6 +790,31 @@ class WC_Tracker { ); } + /** + * Get tracker data for a pickup location method. + * + * @return array Associative array of tracker data with keys: + * - page_contains_block + * - block_attributes + */ + public static function get_pickup_location_data( $block_name, $woo_page_name ) { + $pickup_location_enabled = false; + $pickup_locations_count = count( get_option( 'pickup_location_pickup_locations', [] ) ); + + // Get the available shipping methods + $shipping_methods = WC()->shipping()->get_shipping_methods(); + + // Check if the desired shipping method is enabled + if ( isset( $shipping_methods['pickup_location'] ) && $shipping_methods['pickup_location']->is_enabled()) { + $pickup_location_enabled = true; + } + + return array( + 'pickup_location_enabled' => $pickup_location_enabled, + 'pickup_locations_count' => $pickup_locations_count, + ); + } + /** * Get info about the cart & checkout pages. * @@ -802,6 +827,8 @@ class WC_Tracker { $cart_block_data = self::get_block_tracker_data( 'woocommerce/cart', 'cart' ); $checkout_block_data = self::get_block_tracker_data( 'woocommerce/checkout', 'checkout' ); + $pickup_location_data = self::get_pickup_location_data(); + return array( 'cart_page_contains_cart_shortcode' => self::post_contains_text( $cart_page_id, @@ -816,6 +843,7 @@ class WC_Tracker { 'cart_block_attributes' => $cart_block_data['block_attributes'], 'checkout_page_contains_checkout_block' => $checkout_block_data['page_contains_block'], 'checkout_block_attributes' => $checkout_block_data['block_attributes'], + 'pickup_location' => $pickup_location_data, ); } From c28bb8708ed046ef4cf16e207b34bc69deda0012 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Wed, 15 Feb 2023 16:58:26 +0100 Subject: [PATCH 181/343] fix function call --- plugins/woocommerce/includes/class-wc-tracker.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/woocommerce/includes/class-wc-tracker.php b/plugins/woocommerce/includes/class-wc-tracker.php index 15b99f056b7..1e251144067 100644 --- a/plugins/woocommerce/includes/class-wc-tracker.php +++ b/plugins/woocommerce/includes/class-wc-tracker.php @@ -794,10 +794,10 @@ class WC_Tracker { * Get tracker data for a pickup location method. * * @return array Associative array of tracker data with keys: - * - page_contains_block - * - block_attributes + * - pickup_location_enabled + * - pickup_locations_count */ - public static function get_pickup_location_data( $block_name, $woo_page_name ) { + public static function get_pickup_location_data() { $pickup_location_enabled = false; $pickup_locations_count = count( get_option( 'pickup_location_pickup_locations', [] ) ); From e05a191156aa9c59d8bf94429fba4dbc1a92519e Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Wed, 15 Feb 2023 17:02:55 +0100 Subject: [PATCH 182/343] Use products data store for getPermalinkParts selector (#36706) * Add generated_slug and permalink_template to Product * Update getPermalinkParts to use products data store instead of posts data store * Remove ts-expect-error directive * Changelog * Add resolver for getPermalinkParts --- .../update-get-permalinks-parts-selector | 4 ++ packages/js/data/src/products/index.ts | 1 - packages/js/data/src/products/resolvers.ts | 23 ++++++++- packages/js/data/src/products/selectors.ts | 49 ++++++------------- packages/js/data/src/products/types.ts | 4 ++ 5 files changed, 45 insertions(+), 36 deletions(-) create mode 100644 packages/js/data/changelog/update-get-permalinks-parts-selector diff --git a/packages/js/data/changelog/update-get-permalinks-parts-selector b/packages/js/data/changelog/update-get-permalinks-parts-selector new file mode 100644 index 00000000000..334fc7e1d48 --- /dev/null +++ b/packages/js/data/changelog/update-get-permalinks-parts-selector @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Use products data store for getPermalinkParts selector. diff --git a/packages/js/data/src/products/index.ts b/packages/js/data/src/products/index.ts index 0d3087d3ff5..0d1cd5a38ec 100644 --- a/packages/js/data/src/products/index.ts +++ b/packages/js/data/src/products/index.ts @@ -19,7 +19,6 @@ registerStore< State >( STORE_NAME, { reducer: reducer as Reducer< ProductState >, actions, controls, - // @ts-expect-error as the registerStore type is not allowing the createRegistrySelector selector. selectors, resolvers, } ); diff --git a/packages/js/data/src/products/resolvers.ts b/packages/js/data/src/products/resolvers.ts index 14d07648549..a22ba8f5e86 100644 --- a/packages/js/data/src/products/resolvers.ts +++ b/packages/js/data/src/products/resolvers.ts @@ -2,12 +2,17 @@ * External dependencies */ import { addQueryArgs } from '@wordpress/url'; -import { apiFetch } from '@wordpress/data-controls'; +import { + apiFetch, + dispatch as deprecatedDispatch, + select, +} from '@wordpress/data-controls'; +import { controls } from '@wordpress/data'; /** * Internal dependencies */ -import { WC_PRODUCT_NAMESPACE } from './constants'; +import { STORE_NAME, WC_PRODUCT_NAMESPACE } from './constants'; import { Product, ProductQuery } from './types'; import { getProductError, @@ -19,6 +24,11 @@ import { } from './actions'; import { request } from '../utils'; +const dispatch = + controls && controls.dispatch ? controls.dispatch : deprecatedDispatch; +const resolveSelect = + controls && controls.resolveSelect ? controls.resolveSelect : select; + export function* getProducts( query: Partial< ProductQuery > ) { // id is always required. const productsQuery = { @@ -56,6 +66,11 @@ export function* getProduct( productId: number ) { } ); yield getProductSuccess( productId, product ); + + yield dispatch( STORE_NAME, 'finishResolution', 'getPermalinkParts', [ + productId, + ] ); + return product; } catch ( error ) { yield getProductError( productId, error ); @@ -81,3 +96,7 @@ export function* getProductsTotalCount( query: Partial< ProductQuery > ) { throw error; } } + +export function* getPermalinkParts( productId: number ) { + yield resolveSelect( STORE_NAME, 'getProduct', [ productId ] ); +} diff --git a/packages/js/data/src/products/selectors.ts b/packages/js/data/src/products/selectors.ts index ef1d3cc1bd5..a368fbeb0f7 100644 --- a/packages/js/data/src/products/selectors.ts +++ b/packages/js/data/src/products/selectors.ts @@ -2,7 +2,6 @@ * External dependencies */ import createSelector from 'rememo'; -import { createRegistrySelector } from '@wordpress/data'; /** * Internal dependencies @@ -122,38 +121,24 @@ export const isPending = ( return false; }; -export const getPermalinkParts = createRegistrySelector( - ( select ) => ( state: ProductState, productId: number ) => { - const product = select( 'core' ).getEntityRecord( - 'postType', - 'product', - productId, - // @ts-expect-error query object is not part of the @wordpress/core-data types yet. - { - _fields: [ - 'id', - 'permalink_template', - 'slug', - 'generated_slug', - ], - } +export const getPermalinkParts = ( state: ProductState, productId: number ) => { + const product = state.data[ productId ]; + + if ( product && product.permalink_template ) { + const postName = product.slug || product.generated_slug; + + const [ prefix, suffix ] = product.permalink_template.split( + PERMALINK_PRODUCT_REGEX ); - if ( product && product.permalink_template ) { - const postName = product.slug || product.generated_slug; - const [ prefix, suffix ] = product.permalink_template.split( - PERMALINK_PRODUCT_REGEX - ); - - return { - prefix, - postName, - suffix, - }; - } - return null; + return { + prefix, + postName, + suffix, + }; } -); + return null; +}; export type ProductsSelectors = { getCreateProductError: WPDataSelector< typeof getCreateProductError >; @@ -162,7 +147,5 @@ export type ProductsSelectors = { getProductsTotalCount: WPDataSelector< typeof getProductsTotalCount >; getProductsError: WPDataSelector< typeof getProductsError >; isPending: WPDataSelector< typeof isPending >; - getPermalinkParts: ( - productId: number - ) => { prefix: string; postName: string; suffix: string } | null; + getPermalinkParts: WPDataSelector< typeof getPermalinkParts >; } & WPDataSelectors; diff --git a/packages/js/data/src/products/types.ts b/packages/js/data/src/products/types.ts index 4050775f183..ce341b99962 100644 --- a/packages/js/data/src/products/types.ts +++ b/packages/js/data/src/products/types.ts @@ -66,6 +66,7 @@ export type Product< Status = ProductStatus, Type = ProductType > = Omit< downloads: ProductDownload[]; external_url: string; featured: boolean; + generated_slug: string; id: number; low_stock_amount: number; manage_stock: boolean; @@ -73,6 +74,7 @@ export type Product< Status = ProductStatus, Type = ProductType > = Omit< name: string; on_sale: boolean; permalink: string; + permalink_template: string; price: string; price_html: string; purchasable: boolean; @@ -108,9 +110,11 @@ export const productReadOnlyProperties = [ 'date_created_gmt', 'date_modified', 'date_modified_gmt', + 'generated_slug', 'id', 'on_sale', 'permalink', + 'permalink_template', 'price', 'price_html', 'purchasable', From 9691159d09b21f7524b3f31d9349f5894aaa077c Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Wed, 15 Feb 2023 14:12:17 -0800 Subject: [PATCH 183/343] Switch back to context-less gettext for unit translations While the extra context would be beneficial for translators, these particular strings have already been translated for many lanugages and adding the context would invalidate the translations, which could cause a visual regression for merchants if the new strings don't get reapproved before the next version is released. --- plugins/woocommerce/i18n/units.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/woocommerce/i18n/units.php b/plugins/woocommerce/i18n/units.php index 1bf2a712542..3865b29b662 100644 --- a/plugins/woocommerce/i18n/units.php +++ b/plugins/woocommerce/i18n/units.php @@ -13,16 +13,16 @@ defined( 'ABSPATH' ) || exit; return array( 'weight' => array( - 'kg' => _x( 'kg', 'weight unit kilograms', 'woocommerce' ), - 'g' => _x( 'g', 'weight unit grams', 'woocommerce' ), - 'lbs' => _x( 'lbs', 'weight unit pounds', 'woocommerce' ), - 'oz' => _x( 'oz', 'weight unit ounces', 'woocommerce' ), + 'kg' => __( 'kg', 'woocommerce' ), + 'g' => __( 'g', 'woocommerce' ), + 'lbs' => __( 'lbs', 'woocommerce' ), + 'oz' => __( 'oz', 'woocommerce' ), ), 'dimensions' => array( - 'm' => _x( 'm', 'dimensions unit meters', 'woocommerce' ), - 'cm' => _x( 'cm', 'dimensions unit centimeters', 'woocommerce' ), - 'mm' => _x( 'mm', 'dimensions unit millimeters', 'woocommerce' ), - 'in' => _x( 'in', 'dimensions unit inches', 'woocommerce' ), - 'yd' => _x( 'yd', 'dimensions unit yards', 'woocommerce' ), + 'm' => __( 'm', 'woocommerce' ), + 'cm' => __( 'cm', 'woocommerce' ), + 'mm' => __( 'mm', 'woocommerce' ), + 'in' => __( 'in', 'woocommerce' ), + 'yd' => __( 'yd', 'woocommerce' ), ), ); From b336652e0d46bfa96c1257e5bd2424e25e07d632 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Wed, 15 Feb 2023 15:10:36 -0800 Subject: [PATCH 184/343] Return the input string if it's not a recognized unit of measure Rather than returning an empty string as the label for an unknown unit of measure, this allows a merchant to use a custom unit of measure for something and still have it appear in the UI. --- .../woocommerce/src/Utilities/I18nUtil.php | 10 ++- .../tests/php/src/Utilities/I18nUtilTest.php | 70 ++++++++++++++++--- 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/plugins/woocommerce/src/Utilities/I18nUtil.php b/plugins/woocommerce/src/Utilities/I18nUtil.php index 07a3480565f..7ca4f1e17e0 100644 --- a/plugins/woocommerce/src/Utilities/I18nUtil.php +++ b/plugins/woocommerce/src/Utilities/I18nUtil.php @@ -19,6 +19,9 @@ final class I18nUtil { /** * Get the translated label for a weight unit of measure. * + * This will return the original input string if it isn't found in the units array. This way a custom unit of + * measure can be used even if its' not getting translated. + * * @param string $weight_unit The abbreviated weight unit in English, e.g. kg. * * @return string @@ -28,7 +31,7 @@ final class I18nUtil { self::$units = include WC()->plugin_path() . '/i18n/units.php'; } - $label = ''; + $label = $weight_unit; if ( ! empty( self::$units['weight'][ $weight_unit ] ) ) { $label = self::$units['weight'][ $weight_unit ]; @@ -40,6 +43,9 @@ final class I18nUtil { /** * Get the translated label for a dimensions unit of measure. * + * This will return the original input string if it isn't found in the units array. This way a custom unit of + * measure can be used even if its' not getting translated. + * * @param string $dimensions_unit The abbreviated dimension unit in English, e.g. cm. * * @return string @@ -49,7 +55,7 @@ final class I18nUtil { self::$units = include WC()->plugin_path() . '/i18n/units.php'; } - $label = ''; + $label = $dimensions_unit; if ( ! empty( self::$units['dimensions'][ $dimensions_unit ] ) ) { $label = self::$units['dimensions'][ $dimensions_unit ]; diff --git a/plugins/woocommerce/tests/php/src/Utilities/I18nUtilTest.php b/plugins/woocommerce/tests/php/src/Utilities/I18nUtilTest.php index 56e11e6d4e2..e4488373aab 100644 --- a/plugins/woocommerce/tests/php/src/Utilities/I18nUtilTest.php +++ b/plugins/woocommerce/tests/php/src/Utilities/I18nUtilTest.php @@ -8,43 +8,97 @@ use Automattic\WooCommerce\Utilities\I18nUtil; * A collection of tests for the internationalization utility class. */ class I18nUtilTest extends \WC_Unit_Test_Case { + /** + * A reflected instance of I18nUtil that can be manipulated for testing. + * + * @var \ReflectionClass + */ + private static $reflection; /** - * @testdox `get_weight_unit_label` should return the input when it's a valid weight unit and the locale is en_US. + * Do once before all the tests in this class. + */ + public static function set_up_before_class() { + parent::set_up_before_class(); + + self::$reflection = new \ReflectionClass( I18nUtil::class ); + } + + /** + * Do before each test in this class. + * + * @return void + * @throws \ReflectionException + */ + public function set_up() { + parent::set_up(); + + // Ensure unit strings are not already cached before the gettext filter gets added. + $units_prop = self::$reflection->getProperty( 'units' ); + $units_prop->setAccessible( true ); + $units_prop->setValue( I18nUtil::class, null ); + + add_filter( 'gettext', array( __CLASS__, 'filter_gettext' ) ); + } + + /** + * Do after each test in this class. + * + * @return void + */ + public function tear_down() { + remove_filter( 'gettext', array( __CLASS__, 'filter_gettext' ) ); + + parent::tear_down(); + } + + /** + * Simulate a string getting translated. + * + * @param string $translated + * + * @return string + */ + public static function filter_gettext( $translated ): string { + return sprintf( '%s (translated)', $translated ); + } + + /** + * @testdox `get_weight_unit_label` should return a translated string when it's a valid weight unit. */ public function test_get_valid_weight_unit_label() { $actual = I18nUtil::get_weight_unit_label( 'oz' ); - $expected = 'oz'; + $expected = 'oz (translated)'; $this->assertEquals( $expected, $actual ); } /** - * @testdox `get_weight_unit_label` should return an empty string when the input is an invalid weight unit. + * @testdox `get_weight_unit_label` should return the original input string when the input is not a recognized weight unit. */ public function test_get_invalid_weight_unit_label() { $actual = I18nUtil::get_weight_unit_label( 'chz' ); // Cheezeburgers. - $expected = ''; + $expected = 'chz'; $this->assertEquals( $expected, $actual ); } /** - * @testdox `get_dimensions_unit_label` should return the input when it's a valid dimensions unit and the locale is en_US. + * @testdox `get_dimensions_unit_label` should return a translated string when it's a valid dimensions unit. */ public function test_get_valid_dimensions_unit_label() { $actual = I18nUtil::get_dimensions_unit_label( 'yd' ); - $expected = 'yd'; + $expected = 'yd (translated)'; $this->assertEquals( $expected, $actual ); } /** - * @testdox `get_dimensions_unit_label` should return an empty string when the input is an invalid dimensions unit. + * @testdox `get_dimensions_unit_label` should return the original input string when the input is not a recognized dimensions unit. */ public function test_get_invalid_dimensions_unit_label() { $actual = I18nUtil::get_weight_unit_label( 'pc' ); // Parsecs. - $expected = ''; + $expected = 'pc'; $this->assertEquals( $expected, $actual ); } From 76e726036217a86e2ea910d0b7348e2b1f7e2422 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Wed, 15 Feb 2023 15:20:25 -0800 Subject: [PATCH 185/343] Clean up linting errors --- plugins/woocommerce/tests/php/src/Utilities/I18nUtilTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce/tests/php/src/Utilities/I18nUtilTest.php b/plugins/woocommerce/tests/php/src/Utilities/I18nUtilTest.php index e4488373aab..6a01b509bd1 100644 --- a/plugins/woocommerce/tests/php/src/Utilities/I18nUtilTest.php +++ b/plugins/woocommerce/tests/php/src/Utilities/I18nUtilTest.php @@ -28,7 +28,7 @@ class I18nUtilTest extends \WC_Unit_Test_Case { * Do before each test in this class. * * @return void - * @throws \ReflectionException + * @throws \ReflectionException If the units property does not exist on the class. */ public function set_up() { parent::set_up(); @@ -55,7 +55,7 @@ class I18nUtilTest extends \WC_Unit_Test_Case { /** * Simulate a string getting translated. * - * @param string $translated + * @param string $translated The "translated" string before we modify it. * * @return string */ From e016fb1093680a9e86ba761f7eb4d878b441a177 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Wed, 15 Feb 2023 16:42:10 -0800 Subject: [PATCH 186/343] Update plugins/woocommerce/src/Utilities/I18nUtil.php Co-authored-by: Barry Hughes <3594411+barryhughes@users.noreply.github.com> --- plugins/woocommerce/src/Utilities/I18nUtil.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/src/Utilities/I18nUtil.php b/plugins/woocommerce/src/Utilities/I18nUtil.php index 7ca4f1e17e0..993b8ceecf4 100644 --- a/plugins/woocommerce/src/Utilities/I18nUtil.php +++ b/plugins/woocommerce/src/Utilities/I18nUtil.php @@ -20,7 +20,7 @@ final class I18nUtil { * Get the translated label for a weight unit of measure. * * This will return the original input string if it isn't found in the units array. This way a custom unit of - * measure can be used even if its' not getting translated. + * measure can be used even if it's not getting translated. * * @param string $weight_unit The abbreviated weight unit in English, e.g. kg. * From 23f52e040c51f2901bbad1829d86e8a10402b331 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Wed, 15 Feb 2023 16:42:41 -0800 Subject: [PATCH 187/343] Update plugins/woocommerce/src/Utilities/I18nUtil.php Co-authored-by: Barry Hughes <3594411+barryhughes@users.noreply.github.com> --- plugins/woocommerce/src/Utilities/I18nUtil.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/src/Utilities/I18nUtil.php b/plugins/woocommerce/src/Utilities/I18nUtil.php index 993b8ceecf4..3d1df10a733 100644 --- a/plugins/woocommerce/src/Utilities/I18nUtil.php +++ b/plugins/woocommerce/src/Utilities/I18nUtil.php @@ -44,7 +44,7 @@ final class I18nUtil { * Get the translated label for a dimensions unit of measure. * * This will return the original input string if it isn't found in the units array. This way a custom unit of - * measure can be used even if its' not getting translated. + * measure can be used even if it's not getting translated. * * @param string $dimensions_unit The abbreviated dimension unit in English, e.g. cm. * From 0e7c13d8837b6aaaeafd5fed2df6e81a3fe73105 Mon Sep 17 00:00:00 2001 From: aniketpatel32 <45529004+aniketpatel32@users.noreply.github.com> Date: Thu, 16 Feb 2023 13:45:03 +0530 Subject: [PATCH 188/343] Fix: Ampersand changed to & on product attribute export #36520 (#36525) --- plugins/woocommerce/changelog/update_36520 | 4 ++++ .../includes/export/abstract-wc-csv-exporter.php | 2 +- .../includes/export/class-wc-product-csv-exporter.php | 8 ++++---- 3 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 plugins/woocommerce/changelog/update_36520 diff --git a/plugins/woocommerce/changelog/update_36520 b/plugins/woocommerce/changelog/update_36520 new file mode 100644 index 00000000000..646b9de41ed --- /dev/null +++ b/plugins/woocommerce/changelog/update_36520 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix Ampersand changed to & on product attribute export diff --git a/plugins/woocommerce/includes/export/abstract-wc-csv-exporter.php b/plugins/woocommerce/includes/export/abstract-wc-csv-exporter.php index e5631b9a5a3..d87576f14f7 100644 --- a/plugins/woocommerce/includes/export/abstract-wc-csv-exporter.php +++ b/plugins/woocommerce/includes/export/abstract-wc-csv-exporter.php @@ -467,7 +467,7 @@ abstract class WC_CSV_Exporter { $values_to_implode = array(); foreach ( $values as $value ) { - $value = (string) is_scalar( $value ) ? $value : ''; + $value = (string) is_scalar( $value ) ? html_entity_decode( $value, ENT_QUOTES ) : ''; $values_to_implode[] = str_replace( ',', '\\,', $value ); } diff --git a/plugins/woocommerce/includes/export/class-wc-product-csv-exporter.php b/plugins/woocommerce/includes/export/class-wc-product-csv-exporter.php index 30f6b096ff5..a68ed0186eb 100644 --- a/plugins/woocommerce/includes/export/class-wc-product-csv-exporter.php +++ b/plugins/woocommerce/includes/export/class-wc-product-csv-exporter.php @@ -652,7 +652,7 @@ class WC_Product_CSV_Exporter extends WC_CSV_Batch_Exporter { $this->column_names[ 'attributes:taxonomy' . $i ] = sprintf( __( 'Attribute %d global', 'woocommerce' ), $i ); if ( is_a( $attribute, 'WC_Product_Attribute' ) ) { - $row[ 'attributes:name' . $i ] = wc_attribute_label( $attribute->get_name(), $product ); + $row[ 'attributes:name' . $i ] = html_entity_decode( wc_attribute_label( $attribute->get_name(), $product ), ENT_QUOTES ); if ( $attribute->is_taxonomy() ) { $terms = $attribute->get_terms(); @@ -671,14 +671,14 @@ class WC_Product_CSV_Exporter extends WC_CSV_Batch_Exporter { $row[ 'attributes:visible' . $i ] = $attribute->get_visible(); } else { - $row[ 'attributes:name' . $i ] = wc_attribute_label( $attribute_name, $product ); + $row[ 'attributes:name' . $i ] = html_entity_decode( wc_attribute_label( $attribute_name, $product ), ENT_QUOTES ); if ( 0 === strpos( $attribute_name, 'pa_' ) ) { $option_term = get_term_by( 'slug', $attribute, $attribute_name ); // @codingStandardsIgnoreLine. - $row[ 'attributes:value' . $i ] = $option_term && ! is_wp_error( $option_term ) ? str_replace( ',', '\\,', $option_term->name ) : str_replace( ',', '\\,', $attribute ); + $row[ 'attributes:value' . $i ] = $option_term && ! is_wp_error( $option_term ) ? html_entity_decode( str_replace( ',', '\\,', $option_term->name ), ENT_QUOTES ) : html_entity_decode( str_replace( ',', '\\,', $attribute ), ENT_QUOTES ); $row[ 'attributes:taxonomy' . $i ] = 1; } else { - $row[ 'attributes:value' . $i ] = str_replace( ',', '\\,', $attribute ); + $row[ 'attributes:value' . $i ] = html_entity_decode( str_replace( ',', '\\,', $attribute ), ENT_QUOTES ); $row[ 'attributes:taxonomy' . $i ] = 0; } From a6241d8bc7e555cdeb5df5db43a57511da11b4d1 Mon Sep 17 00:00:00 2001 From: Saad Tarhi Date: Thu, 16 Feb 2023 11:29:11 +0100 Subject: [PATCH 189/343] Bump Woo Blocks version to 9.6.0 in composer.json --- plugins/woocommerce/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json index 3cd2fc01769..563a2093586 100644 --- a/plugins/woocommerce/composer.json +++ b/plugins/woocommerce/composer.json @@ -21,7 +21,7 @@ "maxmind-db/reader": "^1.11", "pelago/emogrifier": "^6.0", "woocommerce/action-scheduler": "3.5.4", - "woocommerce/woocommerce-blocks": "9.4.3" + "woocommerce/woocommerce-blocks": "9.6.0" }, "require-dev": { "automattic/jetpack-changelogger": "^3.3.0", From 469eb7276635afdfe20543bed497cfc1e1dfde86 Mon Sep 17 00:00:00 2001 From: Saad Tarhi Date: Thu, 16 Feb 2023 11:31:19 +0100 Subject: [PATCH 190/343] Run composer update woocommerce/woocommerce-blocks --- plugins/woocommerce/.gitignore | 2 ++ .../bin/composer/mozart/composer.lock | 36 +++++++++---------- .../bin/composer/phpcs/composer.lock | 15 ++++---- .../woocommerce/bin/composer/wp/composer.lock | 14 ++++---- plugins/woocommerce/composer.lock | 14 ++++---- 5 files changed, 41 insertions(+), 40 deletions(-) diff --git a/plugins/woocommerce/.gitignore b/plugins/woocommerce/.gitignore index d0e09157ebd..f75feb4afb2 100644 --- a/plugins/woocommerce/.gitignore +++ b/plugins/woocommerce/.gitignore @@ -30,3 +30,5 @@ i18n/languages/woocommerce.pot # Environment files .wp-env.override.json + +.idea diff --git a/plugins/woocommerce/bin/composer/mozart/composer.lock b/plugins/woocommerce/bin/composer/mozart/composer.lock index 1554fb40d8b..778ea2b6c30 100644 --- a/plugins/woocommerce/bin/composer/mozart/composer.lock +++ b/plugins/woocommerce/bin/composer/mozart/composer.lock @@ -268,16 +268,16 @@ }, { "name": "symfony/console", - "version": "v5.4.17", + "version": "v5.4.19", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "58422fdcb0e715ed05b385f70d3e8b5ed4bbd45f" + "reference": "dccb8d251a9017d5994c988b034d3e18aaabf740" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/58422fdcb0e715ed05b385f70d3e8b5ed4bbd45f", - "reference": "58422fdcb0e715ed05b385f70d3e8b5ed4bbd45f", + "url": "https://api.github.com/repos/symfony/console/zipball/dccb8d251a9017d5994c988b034d3e18aaabf740", + "reference": "dccb8d251a9017d5994c988b034d3e18aaabf740", "shasum": "" }, "require": { @@ -347,7 +347,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.17" + "source": "https://github.com/symfony/console/tree/v5.4.19" }, "funding": [ { @@ -363,7 +363,7 @@ "type": "tidelift" } ], - "time": "2022-12-28T14:15:31+00:00" + "time": "2023-01-01T08:32:19+00:00" }, { "name": "symfony/deprecation-contracts", @@ -434,16 +434,16 @@ }, { "name": "symfony/finder", - "version": "v5.4.17", + "version": "v5.4.19", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "40c08632019838dfb3350f18cf5563b8080055fc" + "reference": "6071aebf810ad13fe8200c224f36103abb37cf1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/40c08632019838dfb3350f18cf5563b8080055fc", - "reference": "40c08632019838dfb3350f18cf5563b8080055fc", + "url": "https://api.github.com/repos/symfony/finder/zipball/6071aebf810ad13fe8200c224f36103abb37cf1f", + "reference": "6071aebf810ad13fe8200c224f36103abb37cf1f", "shasum": "" }, "require": { @@ -477,7 +477,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.17" + "source": "https://github.com/symfony/finder/tree/v5.4.19" }, "funding": [ { @@ -493,7 +493,7 @@ "type": "tidelift" } ], - "time": "2022-12-22T10:31:03+00:00" + "time": "2023-01-14T19:14:44+00:00" }, { "name": "symfony/polyfill-ctype", @@ -1072,16 +1072,16 @@ }, { "name": "symfony/string", - "version": "v5.4.17", + "version": "v5.4.19", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "55733a8664b8853b003e70251c58bc8cb2d82a6b" + "reference": "0a01071610fd861cc160dfb7e2682ceec66064cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/55733a8664b8853b003e70251c58bc8cb2d82a6b", - "reference": "55733a8664b8853b003e70251c58bc8cb2d82a6b", + "url": "https://api.github.com/repos/symfony/string/zipball/0a01071610fd861cc160dfb7e2682ceec66064cb", + "reference": "0a01071610fd861cc160dfb7e2682ceec66064cb", "shasum": "" }, "require": { @@ -1138,7 +1138,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.17" + "source": "https://github.com/symfony/string/tree/v5.4.19" }, "funding": [ { @@ -1154,7 +1154,7 @@ "type": "tidelift" } ], - "time": "2022-12-12T15:54:21+00:00" + "time": "2023-01-01T08:32:19+00:00" } ], "aliases": [], diff --git a/plugins/woocommerce/bin/composer/phpcs/composer.lock b/plugins/woocommerce/bin/composer/phpcs/composer.lock index 0e1a6def19c..7717377063e 100644 --- a/plugins/woocommerce/bin/composer/phpcs/composer.lock +++ b/plugins/woocommerce/bin/composer/phpcs/composer.lock @@ -258,16 +258,16 @@ }, { "name": "sirbrillig/phpcs-changed", - "version": "v2.10.0", + "version": "v2.10.1", "source": { "type": "git", "url": "https://github.com/sirbrillig/phpcs-changed.git", - "reference": "ba049e6f7da40d64056f7b6c4078e87f0f292d6b" + "reference": "a5c3be6ec84395b168e7deadf8b167e50449e7a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sirbrillig/phpcs-changed/zipball/ba049e6f7da40d64056f7b6c4078e87f0f292d6b", - "reference": "ba049e6f7da40d64056f7b6c4078e87f0f292d6b", + "url": "https://api.github.com/repos/sirbrillig/phpcs-changed/zipball/a5c3be6ec84395b168e7deadf8b167e50449e7a3", + "reference": "a5c3be6ec84395b168e7deadf8b167e50449e7a3", "shasum": "" }, "require": { @@ -275,9 +275,8 @@ }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1", - "phpstan/phpstan": "^0.12.33", + "phpstan/phpstan": "1.4.10 || ^1.7", "phpunit/phpunit": "^6.4 || ^9.5", - "sirbrillig/phpcs-import-detection": "^1.1.1", "sirbrillig/phpcs-variable-analysis": "^2.1.3", "squizlabs/php_codesniffer": "^3.2.1" }, @@ -309,9 +308,9 @@ "description": "Run phpcs on files, but only report warnings/errors from lines which were changed.", "support": { "issues": "https://github.com/sirbrillig/phpcs-changed/issues", - "source": "https://github.com/sirbrillig/phpcs-changed/tree/v2.10.0" + "source": "https://github.com/sirbrillig/phpcs-changed/tree/v2.10.1" }, - "time": "2022-03-09T18:16:50+00:00" + "time": "2023-01-25T17:08:01+00:00" }, { "name": "squizlabs/php_codesniffer", diff --git a/plugins/woocommerce/bin/composer/wp/composer.lock b/plugins/woocommerce/bin/composer/wp/composer.lock index d949742c29f..6f8bf7a24a2 100644 --- a/plugins/woocommerce/bin/composer/wp/composer.lock +++ b/plugins/woocommerce/bin/composer/wp/composer.lock @@ -222,16 +222,16 @@ }, { "name": "mck89/peast", - "version": "v1.15.0", + "version": "v1.15.1", "source": { "type": "git", "url": "https://github.com/mck89/peast.git", - "reference": "733cd8f62dcb8239094688063a92766bbfcbf523" + "reference": "cf06286910b7efc9dce7503553ebee314df3d3d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mck89/peast/zipball/733cd8f62dcb8239094688063a92766bbfcbf523", - "reference": "733cd8f62dcb8239094688063a92766bbfcbf523", + "url": "https://api.github.com/repos/mck89/peast/zipball/cf06286910b7efc9dce7503553ebee314df3d3d3", + "reference": "cf06286910b7efc9dce7503553ebee314df3d3d3", "shasum": "" }, "require": { @@ -244,7 +244,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15.0-dev" + "dev-master": "1.15.1-dev" } }, "autoload": { @@ -266,9 +266,9 @@ "description": "Peast is PHP library that generates AST for JavaScript code", "support": { "issues": "https://github.com/mck89/peast/issues", - "source": "https://github.com/mck89/peast/tree/v1.15.0" + "source": "https://github.com/mck89/peast/tree/v1.15.1" }, - "time": "2022-09-13T15:56:53+00:00" + "time": "2023-01-21T13:18:17+00:00" }, { "name": "mustache/mustache", diff --git a/plugins/woocommerce/composer.lock b/plugins/woocommerce/composer.lock index 74a78e06e2d..0dfbc0f3186 100644 --- a/plugins/woocommerce/composer.lock +++ b/plugins/woocommerce/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1c61abfc7c6c9b7795d2c18201e29f08", + "content-hash": "3a1c8e5dff2f1aa920bea4f9f1410bee", "packages": [ { "name": "automattic/jetpack-autoloader", @@ -628,16 +628,16 @@ }, { "name": "woocommerce/woocommerce-blocks", - "version": "v9.4.3", + "version": "v9.6.0", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-blocks.git", - "reference": "26feae05d65ff38f0277bdb0c19f9d293d3cbfc4" + "reference": "dc2a8c0e15c43262b91c972b504aa36d58736277" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/26feae05d65ff38f0277bdb0c19f9d293d3cbfc4", - "reference": "26feae05d65ff38f0277bdb0c19f9d293d3cbfc4", + "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/dc2a8c0e15c43262b91c972b504aa36d58736277", + "reference": "dc2a8c0e15c43262b91c972b504aa36d58736277", "shasum": "" }, "require": { @@ -683,9 +683,9 @@ ], "support": { "issues": "https://github.com/woocommerce/woocommerce-blocks/issues", - "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v9.4.3" + "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v9.6.0" }, - "time": "2023-02-02T10:50:45+00:00" + "time": "2023-02-15T12:26:07+00:00" } ], "packages-dev": [ From cef5009a2194478d13bee66a751c5ec4169041a8 Mon Sep 17 00:00:00 2001 From: Saad Tarhi Date: Thu, 16 Feb 2023 11:34:40 +0100 Subject: [PATCH 191/343] Run "pnpm --filter=woocommerce changelog add" --- plugins/woocommerce/changelog/update-woocommerce-blocks-9.6.0 | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/update-woocommerce-blocks-9.6.0 diff --git a/plugins/woocommerce/changelog/update-woocommerce-blocks-9.6.0 b/plugins/woocommerce/changelog/update-woocommerce-blocks-9.6.0 new file mode 100644 index 00000000000..24b580fc556 --- /dev/null +++ b/plugins/woocommerce/changelog/update-woocommerce-blocks-9.6.0 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update WooCommerce Blocks to 9.6.0 From 0c254c03c341ed8f3cce296b4352855b7bbf7c68 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Thu, 16 Feb 2023 14:31:35 +0100 Subject: [PATCH 192/343] Switched matching of screen ID with the untranslated string. --- plugins/woocommerce/changelog/fix-36474 | 4 ++++ .../woocommerce/includes/admin/class-wc-admin-assets.php | 4 ++-- .../woocommerce/includes/admin/helper/class-wc-helper.php | 8 ++++++-- plugins/woocommerce/includes/admin/wc-admin-functions.php | 3 ++- 4 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-36474 diff --git a/plugins/woocommerce/changelog/fix-36474 b/plugins/woocommerce/changelog/fix-36474 new file mode 100644 index 00000000000..da65ee54bd3 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-36474 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Screen ID matching switched to untranslated 'woocommerce' sctrings. diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php index 1f981c36313..f53510f8b0d 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php @@ -106,11 +106,11 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : * Enqueue scripts. */ public function admin_scripts() { - global $wp_query, $post, $theorder; + global $wp_query, $post, $theorder, $admin_page_hooks; $screen = get_current_screen(); $screen_id = $screen ? $screen->id : ''; - $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) ); + $wc_screen_id = $admin_page_hooks['woocommerce']; $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; $version = Constants::get_constant( 'WC_VERSION' ); diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper.php index b23a6c129f3..5236dd21323 100644 --- a/plugins/woocommerce/includes/admin/helper/class-wc-helper.php +++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper.php @@ -504,9 +504,11 @@ class WC_Helper { * Enqueue admin scripts and styles. */ public static function admin_enqueue_scripts() { + global $admin_page_hooks; + $screen = get_current_screen(); $screen_id = $screen ? $screen->id : ''; - $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) ); + $wc_screen_id = $admin_page_hooks['woocommerce']; if ( $wc_screen_id . '_page_wc-addons' === $screen_id && isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) { wp_enqueue_style( 'woocommerce-helper', WC()->plugin_url() . '/assets/css/helper.css', array(), Constants::get_constant( 'WC_VERSION' ) ); @@ -661,7 +663,9 @@ class WC_Helper { * @param object $screen WP screen object. */ public static function current_screen( $screen ) { - $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) ); + global $admin_page_hooks; + + $wc_screen_id = $admin_page_hooks['woocommerce']; if ( $wc_screen_id . '_page_wc-addons' !== $screen->id ) { return; diff --git a/plugins/woocommerce/includes/admin/wc-admin-functions.php b/plugins/woocommerce/includes/admin/wc-admin-functions.php index 3b1afef89a7..ccc7522b950 100644 --- a/plugins/woocommerce/includes/admin/wc-admin-functions.php +++ b/plugins/woocommerce/includes/admin/wc-admin-functions.php @@ -18,8 +18,9 @@ if ( ! defined( 'ABSPATH' ) ) { * @return array */ function wc_get_screen_ids() { + global $admin_page_hooks; - $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) ); + $wc_screen_id = $admin_page_hooks['woocommerce']; $screen_ids = array( 'toplevel_page_' . $wc_screen_id, $wc_screen_id . '_page_wc-orders', From 465b3d0dd5061e3dc6e0abbc2c33cc25f2b84b9d Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Thu, 16 Feb 2023 15:13:00 +0100 Subject: [PATCH 193/343] Use createSelector for getPermalinkParts selector to cache result (#36848) --- ...update-get-permalink-parts-create-selector | 4 +++ packages/js/data/src/products/selectors.ts | 33 +++++++++++-------- 2 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 packages/js/data/changelog/update-get-permalink-parts-create-selector diff --git a/packages/js/data/changelog/update-get-permalink-parts-create-selector b/packages/js/data/changelog/update-get-permalink-parts-create-selector new file mode 100644 index 00000000000..2be1c04bbea --- /dev/null +++ b/packages/js/data/changelog/update-get-permalink-parts-create-selector @@ -0,0 +1,4 @@ +Significance: patch +Type: performance + +Use createSelector for getPermalinkParts selector, to cache result. diff --git a/packages/js/data/src/products/selectors.ts b/packages/js/data/src/products/selectors.ts index a368fbeb0f7..1ce5f6f81f7 100644 --- a/packages/js/data/src/products/selectors.ts +++ b/packages/js/data/src/products/selectors.ts @@ -121,24 +121,29 @@ export const isPending = ( return false; }; -export const getPermalinkParts = ( state: ProductState, productId: number ) => { - const product = state.data[ productId ]; +export const getPermalinkParts = createSelector( + ( state: ProductState, productId: number ) => { + const product = state.data[ productId ]; - if ( product && product.permalink_template ) { - const postName = product.slug || product.generated_slug; + if ( product && product.permalink_template ) { + const postName = product.slug || product.generated_slug; - const [ prefix, suffix ] = product.permalink_template.split( - PERMALINK_PRODUCT_REGEX - ); + const [ prefix, suffix ] = product.permalink_template.split( + PERMALINK_PRODUCT_REGEX + ); - return { - prefix, - postName, - suffix, - }; + return { + prefix, + postName, + suffix, + }; + } + return null; + }, + ( state, productId ) => { + return [ state.data[ productId ] ]; } - return null; -}; +); export type ProductsSelectors = { getCreateProductError: WPDataSelector< typeof getCreateProductError >; From 8ccfedfa4f59e2aa9a30a09ab97f0b2cf87e56de Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Thu, 16 Feb 2023 15:16:56 +0100 Subject: [PATCH 194/343] Hardcoding the string instead of refering to value ...that's only available later or not at all in some contexts. --- .../woocommerce/includes/admin/class-wc-admin-assets.php | 4 ++-- .../woocommerce/includes/admin/helper/class-wc-helper.php | 8 ++------ plugins/woocommerce/includes/admin/wc-admin-functions.php | 4 +--- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php index f53510f8b0d..ce75609010d 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php @@ -106,11 +106,11 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : * Enqueue scripts. */ public function admin_scripts() { - global $wp_query, $post, $theorder, $admin_page_hooks; + global $wp_query, $post, $theorder; $screen = get_current_screen(); $screen_id = $screen ? $screen->id : ''; - $wc_screen_id = $admin_page_hooks['woocommerce']; + $wc_screen_id = 'woocommerce'; $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; $version = Constants::get_constant( 'WC_VERSION' ); diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper.php index 5236dd21323..2c22d2a10a0 100644 --- a/plugins/woocommerce/includes/admin/helper/class-wc-helper.php +++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper.php @@ -504,11 +504,9 @@ class WC_Helper { * Enqueue admin scripts and styles. */ public static function admin_enqueue_scripts() { - global $admin_page_hooks; - $screen = get_current_screen(); $screen_id = $screen ? $screen->id : ''; - $wc_screen_id = $admin_page_hooks['woocommerce']; + $wc_screen_id = 'woocommerce'; if ( $wc_screen_id . '_page_wc-addons' === $screen_id && isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) { wp_enqueue_style( 'woocommerce-helper', WC()->plugin_url() . '/assets/css/helper.css', array(), Constants::get_constant( 'WC_VERSION' ) ); @@ -663,9 +661,7 @@ class WC_Helper { * @param object $screen WP screen object. */ public static function current_screen( $screen ) { - global $admin_page_hooks; - - $wc_screen_id = $admin_page_hooks['woocommerce']; + $wc_screen_id = 'woocommerce'; if ( $wc_screen_id . '_page_wc-addons' !== $screen->id ) { return; diff --git a/plugins/woocommerce/includes/admin/wc-admin-functions.php b/plugins/woocommerce/includes/admin/wc-admin-functions.php index ccc7522b950..fd2830a0f27 100644 --- a/plugins/woocommerce/includes/admin/wc-admin-functions.php +++ b/plugins/woocommerce/includes/admin/wc-admin-functions.php @@ -18,9 +18,7 @@ if ( ! defined( 'ABSPATH' ) ) { * @return array */ function wc_get_screen_ids() { - global $admin_page_hooks; - - $wc_screen_id = $admin_page_hooks['woocommerce']; + $wc_screen_id = 'woocommerce'; $screen_ids = array( 'toplevel_page_' . $wc_screen_id, $wc_screen_id . '_page_wc-orders', From f615c587144e6b0779d3e3e7615a122807a72db0 Mon Sep 17 00:00:00 2001 From: louwie17 Date: Thu, 16 Feb 2023 10:42:41 -0400 Subject: [PATCH 195/343] Fix create ordered children helper function (#36831) * Still pass injectProps into child props for slot fill helper * Add changelog --- packages/js/components/changelog/fix-create_ordered_children | 4 ++++ packages/js/components/src/utils.tsx | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 packages/js/components/changelog/fix-create_ordered_children diff --git a/packages/js/components/changelog/fix-create_ordered_children b/packages/js/components/changelog/fix-create_ordered_children new file mode 100644 index 00000000000..18b502f176d --- /dev/null +++ b/packages/js/components/changelog/fix-create_ordered_children @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix issue were Options tab was not showing up anymore in new product management screen. diff --git a/packages/js/components/src/utils.tsx b/packages/js/components/src/utils.tsx index 287246ca525..67bb84575b7 100644 --- a/packages/js/components/src/utils.tsx +++ b/packages/js/components/src/utils.tsx @@ -44,7 +44,7 @@ function getChildrenAndProps< T = Fill.Props, S = Record< string, unknown > >( } return { children: children as React.ReactElement< ChildrenProps >, - props: { order }, + props: { order, ...injectProps }, }; } throw Error( 'Invalid children type' ); From 23d5c4d0749a43a21bf7a4ac1d7ab89a168a0aed Mon Sep 17 00:00:00 2001 From: louwie17 Date: Thu, 16 Feb 2023 10:43:23 -0400 Subject: [PATCH 196/343] Update product editor packages (#36815) * Add product section components back to Components package * Fix imports * Add a deprecated message * Add changelog. --- .../changelog/update-product_editor_packages | 4 ++ packages/js/components/src/index.ts | 6 +++ .../src/product-section-layout/index.ts | 2 + .../product-field-section.tsx | 47 +++++++++++++++++ .../product-section-layout.tsx | 44 ++++++++++++++++ .../src/product-section-layout/style.scss | 52 +++++++++++++++++++ packages/js/components/src/style.scss | 1 + 7 files changed, 156 insertions(+) create mode 100644 packages/js/components/changelog/update-product_editor_packages create mode 100644 packages/js/components/src/product-section-layout/index.ts create mode 100644 packages/js/components/src/product-section-layout/product-field-section.tsx create mode 100644 packages/js/components/src/product-section-layout/product-section-layout.tsx create mode 100644 packages/js/components/src/product-section-layout/style.scss diff --git a/packages/js/components/changelog/update-product_editor_packages b/packages/js/components/changelog/update-product_editor_packages new file mode 100644 index 00000000000..5e7d389add7 --- /dev/null +++ b/packages/js/components/changelog/update-product_editor_packages @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Add deprecated message to packages moved to product-editor package. diff --git a/packages/js/components/src/index.ts b/packages/js/components/src/index.ts index 38816ca6c0f..08683120c62 100644 --- a/packages/js/components/src/index.ts +++ b/packages/js/components/src/index.ts @@ -95,3 +95,9 @@ export { SlotContextType, SlotContextHelpersType, } from './slot-context'; + +// Exports below can be removed once the @woocommerce/product-editor package is released. +export { + ProductSectionLayout as __experimentalProductSectionLayout, + ProductFieldSection as __experimentalProductFieldSection, +} from './product-section-layout'; diff --git a/packages/js/components/src/product-section-layout/index.ts b/packages/js/components/src/product-section-layout/index.ts new file mode 100644 index 00000000000..9c670115867 --- /dev/null +++ b/packages/js/components/src/product-section-layout/index.ts @@ -0,0 +1,2 @@ +export * from './product-section-layout'; +export * from './product-field-section'; diff --git a/packages/js/components/src/product-section-layout/product-field-section.tsx b/packages/js/components/src/product-section-layout/product-field-section.tsx new file mode 100644 index 00000000000..d06d03669b9 --- /dev/null +++ b/packages/js/components/src/product-section-layout/product-field-section.tsx @@ -0,0 +1,47 @@ +/** + * External dependencies + */ +import { createElement } from '@wordpress/element'; +import { Card, CardBody } from '@wordpress/components'; +import deprecated from '@wordpress/deprecated'; + +/** + * Internal dependencies + */ +import { ProductSectionLayout } from './product-section-layout'; +import { WooProductFieldItem } from '../woo-product-field-item'; + +type ProductFieldSectionProps = { + id: string; + title: string; + description: string | JSX.Element; + className?: string; +}; + +export const ProductFieldSection: React.FC< ProductFieldSectionProps > = ( { + id, + title, + description, + className, + children, +} ) => { + deprecated( `__experimentalProductFieldSection`, { + version: '13.0.0', + plugin: '@woocommerce/components', + hint: 'Moved to @woocommerce/product-editor package: import { __experimentalProductFieldSection } from @woocommerce/product-editor', + } ); + return ( + + + + { children } + + + + + ); +}; diff --git a/packages/js/components/src/product-section-layout/product-section-layout.tsx b/packages/js/components/src/product-section-layout/product-section-layout.tsx new file mode 100644 index 00000000000..66c8c84cb68 --- /dev/null +++ b/packages/js/components/src/product-section-layout/product-section-layout.tsx @@ -0,0 +1,44 @@ +/** + * External dependencies + */ +import { Children, isValidElement, createElement } from '@wordpress/element'; +import deprecated from '@wordpress/deprecated'; +/** + * Internal dependencies + */ +import { FormSection } from '../form-section'; + +type ProductSectionLayoutProps = { + title: string; + description: string | JSX.Element; + className?: string; +}; + +export const ProductSectionLayout: React.FC< ProductSectionLayoutProps > = ( { + title, + description, + className, + children, +} ) => { + deprecated( `__experimentalProductSectionLayout`, { + version: '13.0.0', + plugin: '@woocommerce/components', + hint: 'Moved to @woocommerce/product-editor package: import { __experimentalProductSectionLayout } from @woocommerce/product-editor', + } ); + return ( + + { Children.map( children, ( child ) => { + if ( isValidElement( child ) && child.props.onChange ) { + return ( +
{ child }
+ ); + } + return child; + } ) } +
+ ); +}; diff --git a/packages/js/components/src/product-section-layout/style.scss b/packages/js/components/src/product-section-layout/style.scss new file mode 100644 index 00000000000..64753f99ecd --- /dev/null +++ b/packages/js/components/src/product-section-layout/style.scss @@ -0,0 +1,52 @@ +.woocommerce-form-section { + a { + text-decoration: none; + } + + &__content { + .components-card { + border: 1px solid $gray-400; + border-radius: 2px; + box-shadow: none; + &__body { + padding: $gap-large; + + .components-base-control, + .components-dropdown, + .woocommerce-rich-text-editor { + &:not(:first-child):not(.components-radio-control) { + margin-top: $gap-large - $gap-smaller; + margin-bottom: 0; + } + } + } + } + + .woocommerce-product-form__field:not(:first-child) { + margin-top: $gap-large; + + > .components-base-control { + margin-bottom: 0; + } + } + + .components-radio-control .components-v-stack { + gap: $gap-small; + } + + .woocommerce-collapsible-content { + margin-top: $gap-large; + } + } + + &__header { + p > span { + display: block; + margin-bottom: $gap-smaller; + } + } + + &:not(:first-child) { + margin-top: $gap-largest; + } +} diff --git a/packages/js/components/src/style.scss b/packages/js/components/src/style.scss index be220285b73..c6f9ef9ed81 100644 --- a/packages/js/components/src/style.scss +++ b/packages/js/components/src/style.scss @@ -56,3 +56,4 @@ @import 'collapsible-content/style.scss'; @import 'form/style.scss'; @import 'experimental-tree-control/tree.scss'; +@import 'product-section-layout/style.scss'; From ab6d3a45af526ed6961a3ce71f996610ff37b34f Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Thu, 16 Feb 2023 11:52:12 -0300 Subject: [PATCH 197/343] Show attribute creation form when there are no attributes (#36606) * Add attribute creation form * Add changelog * Check `Used for variations` by default * Fix lint * Fix lint * Fix `create-variable-product.spec` e2e tests * Fix do not show form with local attributes * Fix lint * Fix tests * Refactor e2e test fix * Replaced `wp_kses_post` with `esc_html_e` * Fix lint --------- Co-authored-by: Fernando Marichal --- .../dev-35115_show_attribute_creation_form | 4 +++ .../legacy/js/admin/meta-boxes-product.js | 6 ++++ .../includes/admin/class-wc-admin-assets.php | 2 ++ .../views/html-product-data-attributes.php | 32 +++++++++++++------ .../woocommerce/includes/class-wc-ajax.php | 4 ++- .../merchant/create-variable-product.spec.js | 10 +++--- 6 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 plugins/woocommerce/changelog/dev-35115_show_attribute_creation_form diff --git a/plugins/woocommerce/changelog/dev-35115_show_attribute_creation_form b/plugins/woocommerce/changelog/dev-35115_show_attribute_creation_form new file mode 100644 index 00000000000..c4240015e47 --- /dev/null +++ b/plugins/woocommerce/changelog/dev-35115_show_attribute_creation_form @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Add attribute creation form when there are no attributes 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 bebb8f422f0..246fc6178f8 100644 --- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js +++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js @@ -57,6 +57,12 @@ jQuery( function ( $ ) { } ); } ); + $( function () { + if ( ! woocommerce_admin_meta_boxes.has_attributes ) { + $( 'button.add_attribute' ).trigger( 'click' ); + } + } ); + // Catalog Visibility. $( '#catalog-visibility' ) .find( '.edit-catalog-visibility' ) diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php index 1f981c36313..5754b93d9cd 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php @@ -339,6 +339,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : $remove_item_notice = __( 'Are you sure you want to remove the selected items?', 'woocommerce' ); $remove_fee_notice = __( 'Are you sure you want to remove the selected fees?', 'woocommerce' ); $remove_shipping_notice = __( 'Are you sure you want to remove the selected shipping?', 'woocommerce' ); + $product = wc_get_product( $post_id ); // Eventually this will become wc_data_or_post object as we implement more custom tables. $order_or_post_object = $post; @@ -403,6 +404,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : 'rounding_precision' => wc_get_rounding_precision(), 'tax_rounding_mode' => wc_get_tax_rounding_mode(), 'product_types' => array_unique( array_merge( array( 'simple', 'grouped', 'variable', 'external' ), array_keys( wc_get_product_types() ) ) ), + 'has_attributes' => ! empty( wc_get_attribute_taxonomies() ) || ! empty( $product ? $product->get_attributes( 'edit' ) : array() ), 'i18n_download_permission_fail' => __( 'Could not grant access - the user may already have permission for this file or billing email is not set. Ensure the billing email is set, and the order has been saved.', 'woocommerce' ), 'i18n_permission_revoke' => __( 'Are you sure you want to revoke access to this download?', 'woocommerce' ), 'i18n_tax_rate_already_exists' => __( 'You cannot add the same tax rate twice!', 'woocommerce' ), diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-attributes.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-attributes.php index 1f74bd78134..bf84ef7e988 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-attributes.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-attributes.php @@ -5,6 +5,26 @@ if ( ! defined( 'ABSPATH' ) ) { ?>