From 7daf26ce39f51ac2a7f50fa684b37987fd8d8d2e Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Mon, 24 Oct 2022 15:44:13 -0700 Subject: [PATCH 001/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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/228] 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 4fa4f802e947ba2502a857514ad7eefb82d8adc7 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 23 Dec 2022 21:19:00 +0800 Subject: [PATCH 033/228] 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 034/228] 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 035/228] 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 036/228] 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 037/228] 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 038/228] 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 039/228] 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 040/228] 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 6415f3f9111cf4bf115f684181d0140cd86967f2 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:23:26 +0000 Subject: [PATCH 041/228] 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 042/228] 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 043/228] 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 044/228] 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 045/228] 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 046/228] 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 047/228] 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 048/228] 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 049/228] 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 050/228] 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 051/228] 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 052/228] 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 053/228] 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 054/228] 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 055/228] 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 056/228] 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 057/228] 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 058/228] 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 059/228] 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 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 060/228] 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 7e27c0d67ac1aab6b2d19969a2f20e1acca24df8 Mon Sep 17 00:00:00 2001 From: Nima Date: Mon, 16 Jan 2023 17:12:16 +0000 Subject: [PATCH 061/228] 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 062/228] 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 063/228] 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 7cf15992086dbbedd1c74c310b9869948ce77cbf Mon Sep 17 00:00:00 2001 From: nigeljamesstevenson Date: Wed, 18 Jan 2023 17:12:55 +0000 Subject: [PATCH 064/228] update smoke test daily workflow to run api tests first --- .github/workflows/smoke-test-daily.yml | 93 ++++++++++--------- ...kflow-run-API-tests-first-before-E2E-tests | 5 + 2 files changed, 52 insertions(+), 46 deletions(-) create mode 100644 plugins/woocommerce/changelog/update-smoke-test-daily-workflow-run-API-tests-first-before-E2E-tests diff --git a/.github/workflows/smoke-test-daily.yml b/.github/workflows/smoke-test-daily.yml index 7c5b9ea78ea..8643b6d395b 100644 --- a/.github/workflows/smoke-test-daily.yml +++ b/.github/workflows/smoke-test-daily.yml @@ -17,11 +17,57 @@ concurrency: permissions: {} jobs: + api-tests: + name: API tests on nightly build + runs-on: ubuntu-20.04 + permissions: + contents: read + if: success() || failure() + env: + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-report + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ env.BRANCH_NAME }} + + - name: Setup WooCommerce Monorepo + uses: ./.github/actions/setup-woocommerce-monorepo + with: + install-filters: woocommerce + build: false + + - name: Run API tests. + working-directory: plugins/woocommerce + env: + BASE_URL: ${{ secrets.SMOKE_TEST_URL }} + USER_KEY: ${{ secrets.SMOKE_TEST_ADMIN_USER }} + USER_SECRET: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }} + DEFAULT_TIMEOUT_OVERRIDE: 120000 + run: pnpm exec playwright test --config=tests/api-core-tests/playwright.config.js hello.test.js + + - name: Generate API Test report. + if: success() || failure() + working-directory: plugins/woocommerce + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} + + - name: Archive API test report + if: success() || failure() + uses: actions/upload-artifact@v3 + with: + name: ${{ env.API_ARTIFACT }} + path: | + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} + if-no-files-found: ignore + retention-days: 5 + e2e-tests: name: E2E tests on nightly build runs-on: ubuntu-20.04 permissions: contents: read + needs: [api-tests] env: ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }} ADMIN_USER: ${{ secrets.SMOKE_TEST_ADMIN_USER }} @@ -78,52 +124,6 @@ jobs: if-no-files-found: ignore retention-days: 5 - api-tests: - name: API tests on nightly build - runs-on: ubuntu-20.04 - permissions: - contents: read - needs: [e2e-tests] - if: success() || failure() - env: - ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-results - ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-report - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ env.BRANCH_NAME }} - - - name: Setup WooCommerce Monorepo - uses: ./.github/actions/setup-woocommerce-monorepo - with: - install-filters: woocommerce - build: false - - - name: Run API tests. - working-directory: plugins/woocommerce - env: - BASE_URL: ${{ secrets.SMOKE_TEST_URL }} - USER_KEY: ${{ secrets.SMOKE_TEST_ADMIN_USER }} - USER_SECRET: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }} - DEFAULT_TIMEOUT_OVERRIDE: 120000 - run: pnpm exec playwright test --config=tests/api-core-tests/playwright.config.js hello.test.js - - - name: Generate API Test report. - if: success() || failure() - working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} - - - name: Archive API test report - if: success() || failure() - uses: actions/upload-artifact@v3 - with: - name: ${{ env.API_ARTIFACT }} - path: | - ${{ env.ALLURE_RESULTS_DIR }} - ${{ env.ALLURE_REPORT_DIR }} - if-no-files-found: ignore - retention-days: 5 - k6-tests: name: k6 tests on nightly build runs-on: ubuntu-20.04 @@ -181,6 +181,7 @@ jobs: runs-on: ubuntu-20.04 permissions: contents: read + needs: [api-tests] env: USE_WP_ENV: 1 ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results diff --git a/plugins/woocommerce/changelog/update-smoke-test-daily-workflow-run-API-tests-first-before-E2E-tests b/plugins/woocommerce/changelog/update-smoke-test-daily-workflow-run-API-tests-first-before-E2E-tests new file mode 100644 index 00000000000..8e9c06e0c30 --- /dev/null +++ b/plugins/woocommerce/changelog/update-smoke-test-daily-workflow-run-API-tests-first-before-E2E-tests @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: Update smoke test daily workflow to run api tests before e2e tests + + From 2d7c8db2e5b49c2d268e3a2d43605c777ec89d35 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Thu, 19 Jan 2023 01:32:38 +0800 Subject: [PATCH 065/228] 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 066/228] 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 067/228] 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 068/228] 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 069/228] 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 070/228] 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 072/228] 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 078/228] 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: Fri, 9 Dec 2022 02:19:36 +0800 Subject: [PATCH 079/228] Code refactor with CenteredSpinner. (cherry picked from commit 52166434658e68e25f2d1536448a56376e67ac1e) --- .../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 0fe028d8c2b61a3fb8dba8ff08daaa845b53facc Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 9 Dec 2022 19:30:24 +0800 Subject: [PATCH 080/228] Code refactor with CardHeaderTitle component. This component will be used in other components later. (cherry picked from commit 995fb7e02d80b721376562727416317534ca177d) --- .../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 } + + { expanded && + recommendedChannels.map( ( el, idx ) => { + return ( + + + { idx < recommendedChannels.length - 1 && ( + + ) } + + ); + } ) } +
) } ); diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.scss deleted file mode 100644 index 9a02c3861dc..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.scss +++ /dev/null @@ -1,8 +0,0 @@ -.woocommerce-marketing-recommended-channels { - .components-button.is-link { - font-size: 14px; - font-weight: 600; - line-height: 17px; - text-decoration: none; - } -} diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.tsx deleted file mode 100644 index b359af6ecb5..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/** - * External dependencies - */ -import { 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 { RecommendedChannel } from '~/marketing/data-multichannel/types'; -import { RecommendedChannelsList } from './RecommendedChannelsList'; -import './RecommendedChannels.scss'; - -type RecommendedChannelsType = { - recommendedChannels: Array< RecommendedChannel >; - onInstalledAndActivated?: () => void; -}; - -export const RecommendedChannels: React.FC< RecommendedChannelsType > = ( { - recommendedChannels, - onInstalledAndActivated, -} ) => { - const [ collapsed, setCollapsed ] = useState( true ); - - return ( -
- - - - - { ! 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 deleted file mode 100644 index dac8d07d386..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/** - * 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 '~/marketing/data-multichannel/types'; -import './Channels.scss'; - -type RecommendedChannelListPropsType = { - recommendedChannels: Array< RecommendedChannel >; - onInstalledAndActivated?: () => void; -}; - -export const RecommendedChannelsList: React.FC< - RecommendedChannelListPropsType -> = ( { recommendedChannels, onInstalledAndActivated } ) => { - return ( - <> - { recommendedChannels.map( ( el, idx ) => { - return ( - - - { idx < recommendedChannels.length - 1 && ( - - ) } - - ); - } ) } - - ); -}; From 15200103ccb5fd936fd50d178b289f880a62e14f Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 28 Jan 2023 01:16:31 +0800 Subject: [PATCH 115/228] Simplify Channels CSS. --- .../marketing/overview-multichannel/Channels/Channels.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 bfa5c4e55b6..276caba3969 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss @@ -6,9 +6,8 @@ } .components-button.is-link { - font-size: 14px; + @include font-size( 14 ); font-weight: 600; - line-height: 17px; text-decoration: none; } } From c12ae8033dcf760f7b91f70d79e3659892d440f4 Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Fri, 27 Jan 2023 10:59:39 -0800 Subject: [PATCH 116/228] Tweak product link description and display (#36591) * Tweak product link description and display * Add changelog entry * Fix up edit product link modal tests * Update changelog entry --- .../edit-product-link-modal.scss | 8 +++++++ .../edit-product-link-modal.tsx | 22 +++++++++++++------ .../test/edit-product-link-modal.test.tsx | 4 ++-- plugins/woocommerce/changelog/update-36316 | 4 ++++ 4 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 plugins/woocommerce/changelog/update-36316 diff --git a/plugins/woocommerce-admin/client/products/shared/edit-product-link-modal/edit-product-link-modal.scss b/plugins/woocommerce-admin/client/products/shared/edit-product-link-modal/edit-product-link-modal.scss index 38ed78ef5dc..ef963622ac3 100644 --- a/plugins/woocommerce-admin/client/products/shared/edit-product-link-modal/edit-product-link-modal.scss +++ b/plugins/woocommerce-admin/client/products/shared/edit-product-link-modal/edit-product-link-modal.scss @@ -8,4 +8,12 @@ gap: 8px; justify-content: flex-end; } + + .components-modal__header { + border-color: $gray-300; + } + + .woocommerce-product-link-edit-modal__description { + margin: $gap-large 0; + } } diff --git a/plugins/woocommerce-admin/client/products/shared/edit-product-link-modal/edit-product-link-modal.tsx b/plugins/woocommerce-admin/client/products/shared/edit-product-link-modal/edit-product-link-modal.tsx index 72c91c1f95f..7ad4d6baf1e 100644 --- a/plugins/woocommerce-admin/client/products/shared/edit-product-link-modal/edit-product-link-modal.tsx +++ b/plugins/woocommerce-admin/client/products/shared/edit-product-link-modal/edit-product-link-modal.tsx @@ -6,6 +6,7 @@ import { Button, Modal, TextControl } from '@wordpress/components'; import { useState } from '@wordpress/element'; import { useDispatch } from '@wordpress/data'; import { cleanForSlug } from '@wordpress/url'; +import interpolateComponents from '@automattic/interpolate-components'; import { Product } from '@woocommerce/data'; import { Text } from '@woocommerce/experimental'; import { useFormContext } from '@woocommerce/components'; @@ -95,18 +96,25 @@ export const EditProductLinkModal: React.FC< EditProductLinkModalProps > = ( { className="woocommerce-product-link-edit-modal" >
+

+ { __( + "Create a unique link for this product. Use simple, descriptive words and numbers. We'll replace spaces with hyphens (-).", + 'woocommerce' + ) } +

{ newProductLinkLabel }, + }, + } ) } /> - - { __( - "Use simple, descriptive words and numbers. We'll replace spaces with hyphens (-).", - 'woocommerce' - ) } -
- +
+ { hasRegisteredChannels && ( + <> + + + + + + ) } { expanded && recommendedChannels.map( ( el, idx ) => { return ( @@ -134,7 +105,8 @@ export const Channels: React.FC< ChannelsProps > = ( { onInstalledAndActivated } /> - { idx < recommendedChannels.length - 1 && ( + { idx !== + recommendedChannels.length - 1 && ( ) } From 6c053d3eeca2805658f45a15e57ddd96d78bc1ab Mon Sep 17 00:00:00 2001 From: Atanas Penchev Date: Mon, 30 Jan 2023 07:48:20 +0200 Subject: [PATCH 119/228] Replace $.ajax() calls with browser-native window.fetch() calls. (#36275) --- .../changelog/fetch-instead-of-jquery-ajax | 4 + .../js/frontend/add-to-cart-variation.js | 28 +++- .../client/legacy/js/frontend/add-to-cart.js | 22 +++- .../legacy/js/frontend/cart-fragments.js | 24 +++- .../client/legacy/js/frontend/cart.js | 57 ++++++--- .../client/legacy/js/frontend/checkout.js | 120 +++++++++++------- .../client/legacy/js/frontend/geolocation.js | 12 +- 7 files changed, 194 insertions(+), 73 deletions(-) create mode 100644 plugins/woocommerce/changelog/fetch-instead-of-jquery-ajax diff --git a/plugins/woocommerce/changelog/fetch-instead-of-jquery-ajax b/plugins/woocommerce/changelog/fetch-instead-of-jquery-ajax new file mode 100644 index 00000000000..e99a93aab17 --- /dev/null +++ b/plugins/woocommerce/changelog/fetch-instead-of-jquery-ajax @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Replace $.ajax() calls with browser-native window.fetch() calls. diff --git a/plugins/woocommerce/client/legacy/js/frontend/add-to-cart-variation.js b/plugins/woocommerce/client/legacy/js/frontend/add-to-cart-variation.js index 24e32dbe3a3..29b5086feda 100644 --- a/plugins/woocommerce/client/legacy/js/frontend/add-to-cart-variation.js +++ b/plugins/woocommerce/client/legacy/js/frontend/add-to-cart-variation.js @@ -167,16 +167,16 @@ if ( attributes.count && attributes.count === attributes.chosenCount ) { if ( form.useAjax ) { - if ( form.xhr ) { - form.xhr.abort(); + if ( form.controller ) { + form.controller.abort(); } form.$form.block( { message: null, overlayCSS: { background: '#fff', opacity: 0.6 } } ); currentAttributes.product_id = parseInt( form.$form.data( 'product_id' ), 10 ); currentAttributes.custom_data = form.$form.data( 'custom_data' ); - form.xhr = $.ajax( { + const options = { url: wc_add_to_cart_variation_params.wc_ajax_url.toString().replace( '%%endpoint%%', 'get_variation' ), type: 'POST', - data: currentAttributes, + data: $.param( currentAttributes ), success: function( variation ) { if ( variation ) { form.$form.trigger( 'found_variation', [ variation ] ); @@ -199,7 +199,25 @@ complete: function() { form.$form.unblock(); } - } ); + }; + + const controller = new AbortController(); + form.controller = controller; + + window.fetch( options.url, { + method: options.type, + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, + body: options.data, + signal: controller.signal + } ) + .then( response => { + if ( !response.ok ) { + throw new Error( response.statusText ); + } + return response.json(); + }) + .then( options.success ) + .finally( () => options.complete() ); } else { form.$form.trigger( 'update_variation_values' ); diff --git a/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js b/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js index e27baf072a4..dac6676e560 100644 --- a/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js +++ b/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js @@ -51,7 +51,21 @@ jQuery( function( $ ) { } }; - $.ajax( this.requests[0] ); + const options = this.requests[0]; + window.fetch( options.url, { + method: options.type, + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, + body: options.data + } ) + .then( response => { + if ( !response.ok ) { + throw new Error( response.statusText ); + } + return response.json(); + } ) + .then( options.success ) + .catch( error => options.error && options.error() ) + .finally( () => options.complete && options.complete() ); }; /** @@ -95,7 +109,7 @@ jQuery( function( $ ) { e.data.addToCartHandler.addRequest({ type: 'POST', url: wc_add_to_cart_params.wc_ajax_url.toString().replace( '%%endpoint%%', 'add_to_cart' ), - data: data, + data: $.param( data ), success: function( response ) { if ( ! response ) { return; @@ -139,9 +153,9 @@ jQuery( function( $ ) { e.data.addToCartHandler.addRequest({ type: 'POST', url: wc_add_to_cart_params.wc_ajax_url.toString().replace( '%%endpoint%%', 'remove_from_cart' ), - data: { + data: new URLSearchParams( { cart_item_key : $thisbutton.data( 'cart_item_key' ) - }, + } ).toString(), success: function( response ) { if ( ! response || ! response.fragments ) { window.location = $thisbutton.attr( 'href' ); diff --git a/plugins/woocommerce/client/legacy/js/frontend/cart-fragments.js b/plugins/woocommerce/client/legacy/js/frontend/cart-fragments.js index c5eef7b4c72..b57b2618d84 100644 --- a/plugins/woocommerce/client/legacy/js/frontend/cart-fragments.js +++ b/plugins/woocommerce/client/legacy/js/frontend/cart-fragments.js @@ -38,9 +38,9 @@ jQuery( function( $ ) { var $fragment_refresh = { url: wc_cart_fragments_params.wc_ajax_url.toString().replace( '%%endpoint%%', 'get_refreshed_fragments' ), type: 'POST', - data: { + data: new URLSearchParams( { time: new Date().getTime() - }, + } ).toString(), timeout: wc_cart_fragments_params.request_timeout, success: function( data ) { if ( data && data.fragments ) { @@ -68,7 +68,25 @@ jQuery( function( $ ) { /* Named callback for refreshing cart fragment */ function refresh_cart_fragment() { - $.ajax( $fragment_refresh ); + const controller = new AbortController(); + const timeoutId = setTimeout( () => controller.abort(), $fragment_refresh.timeout ); + + window.fetch( $fragment_refresh.url, { + method: $fragment_refresh.type, + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, + body: $fragment_refresh.data, + signal: controller.signal + } ) + .then( response => { + clearTimeout( timeoutId ); + + if ( !response.ok ) { + throw new Error( response.statusText ); + } + return response.json(); + } ) + .then( $fragment_refresh.success ) + .catch( error => $fragment_refresh.error() ); } /* Cart Handling */ diff --git a/plugins/woocommerce/client/legacy/js/frontend/cart.js b/plugins/woocommerce/client/legacy/js/frontend/cart.js index cdee33f5d79..62524a6bd1e 100644 --- a/plugins/woocommerce/client/legacy/js/frontend/cart.js +++ b/plugins/woocommerce/client/legacy/js/frontend/cart.js @@ -8,6 +8,27 @@ jQuery( function( $ ) { // Utility functions for the file. + /** + * Perform an AJAX request that expects an HTML response. + * + * @param {Object} options + */ + const ajax = options => { + window.fetch( options.url, { + method: options.type || 'GET', + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, + body: options.data + } ) + .then( response => { + if ( !response.ok ) { + throw new Error( response.statusText ); + } + return response.text(); + } ) + .then( options.success ) + .finally( () => options.complete() ); + }; + /** * Gets a url for a given AJAX endpoint. * @@ -222,13 +243,17 @@ jQuery( function( $ ) { var data = { security: wc_cart_params.update_shipping_method_nonce, - shipping_method: shipping_methods }; - $.ajax( { + // Flatten shipping_methods for use in URLSearchParams() + for ( var k in shipping_methods ) { + data[ 'shipping_method[' + k + ']' ] = shipping_methods[ k ]; + } + + ajax( { type: 'post', url: get_url( 'update_shipping_method' ), - data: data, + data: new URLSearchParams( data ).toString(), dataType: 'html', success: function( response ) { update_cart_totals_div( response ); @@ -260,10 +285,10 @@ jQuery( function( $ ) { .appendTo( $form ); // Make call to actual form post URL. - $.ajax( { + ajax( { type: $form.attr( 'method' ), url: $form.attr( 'action' ), - data: $form.serialize(), + data: new URLSearchParams( new FormData( $form[0] ) ).toString(), dataType: 'html', success: function( response ) { update_wc_div( response ); @@ -347,10 +372,10 @@ jQuery( function( $ ) { block( $( 'div.cart_totals' ) ); // Make call to actual form post URL. - $.ajax( { + ajax( { type: $form.attr( 'method' ), url: $form.attr( 'action' ), - data: $form.serialize(), + data: new URLSearchParams( new FormData( $form[0] ) ).toString(), dataType: 'html', success: function( response ) { update_wc_div( response, preserve_notices ); @@ -369,7 +394,7 @@ jQuery( function( $ ) { update_cart_totals: function() { block( $( 'div.cart_totals' ) ); - $.ajax( { + ajax( { url: get_url( 'get_cart_totals' ), dataType: 'html', success: function( response ) { @@ -471,10 +496,10 @@ jQuery( function( $ ) { coupon_code: coupon_code }; - $.ajax( { + ajax( { type: 'POST', url: get_url( 'apply_coupon' ), - data: data, + data: new URLSearchParams( data ).toString(), dataType: 'html', success: function( response ) { $( '.woocommerce-error, .woocommerce-message, .woocommerce-info' ).remove(); @@ -508,10 +533,10 @@ jQuery( function( $ ) { coupon: coupon }; - $.ajax( { + ajax( { type: 'POST', url: get_url( 'remove_coupon' ), - data: data, + data: new URLSearchParams( data ).toString(), dataType: 'html', success: function( response ) { $( '.woocommerce-error, .woocommerce-message, .woocommerce-info' ).remove(); @@ -541,10 +566,10 @@ jQuery( function( $ ) { .appendTo( $form ); // Make call to actual form post URL. - $.ajax( { + ajax( { type: $form.attr( 'method' ), url: $form.attr( 'action' ), - data: $form.serialize(), + data: new URLSearchParams( new FormData( $form[0] ) ).toString(), dataType: 'html', success: function( response ) { update_wc_div( response ); @@ -571,7 +596,7 @@ jQuery( function( $ ) { block( $form ); block( $( 'div.cart_totals' ) ); - $.ajax( { + ajax( { type: 'GET', url: $a.attr( 'href' ), dataType: 'html', @@ -600,7 +625,7 @@ jQuery( function( $ ) { block( $form ); block( $( 'div.cart_totals' ) ); - $.ajax( { + ajax( { type: 'GET', url: $a.attr( 'href' ), dataType: 'html', diff --git a/plugins/woocommerce/client/legacy/js/frontend/checkout.js b/plugins/woocommerce/client/legacy/js/frontend/checkout.js index 91a33b69329..ff9044164c2 100644 --- a/plugins/woocommerce/client/legacy/js/frontend/checkout.js +++ b/plugins/woocommerce/client/legacy/js/frontend/checkout.js @@ -8,6 +8,38 @@ jQuery( function( $ ) { $.blockUI.defaults.overlayCSS.cursor = 'default'; + const ajax = options => { + const controller = new AbortController(); + + window.fetch( options.url, { + method: options.type, + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, + body: options.data, + signal: controller.signal + } ) + .then( response => { + response.text().then( text => { + if ( !response.ok ) { + const error = new Error( response.statusText ); + error.responseText = text; // Needed for when wc_checkout_params.debug_mode is enabled + throw error; + } + + if ( options.dataType === 'html' ) { + options.success( text ); + return; + } + + const json = JSON.parse( ajax.dataFilter( text, 'json' ) ); + options.success( json ); + } ); + } ) + .catch( error => options.error && options.error( error.responseText, 'error', error.message ) ); + + return controller; + }; + ajax.dataFilter = ( raw_response, dataType ) => raw_response; + var wc_checkout_form = { updateTimer: false, dirtyInput: false, @@ -256,11 +288,12 @@ jQuery( function( $ ) { wc_checkout_form.updateTimer = setTimeout( wc_checkout_form.update_checkout_action, '5', args ); }, update_checkout_action: function( args ) { - if ( wc_checkout_form.xhr ) { - wc_checkout_form.xhr.abort(); + if ( wc_checkout_form.controller ) { + wc_checkout_form.controller.abort(); } - if ( $( 'form.checkout' ).length === 0 ) { + var $form = $( 'form.checkout' ); + if ( $form.length === 0 ) { return; } @@ -316,7 +349,7 @@ jQuery( function( $ ) { s_address : s_address, s_address_2 : s_address_2, has_full_address: has_full_address, - post_data : $( 'form.checkout' ).serialize() + post_data : new URLSearchParams( new FormData( $form[0] ) ).toString() }; if ( false !== args.update_shipping_method ) { @@ -327,7 +360,10 @@ jQuery( function( $ ) { shipping_methods[ $( this ).data( 'index' ) ] = $( this ).val(); } ); - data.shipping_method = shipping_methods; + // Flatten shipping_methods for use in URLSearchParams() + for ( var k in shipping_methods ) { + data[ 'shipping_method[' + k + ']' ] = shipping_methods[ k ]; + } } $( '.woocommerce-checkout-payment, .woocommerce-checkout-review-order-table' ).block({ @@ -338,10 +374,10 @@ jQuery( function( $ ) { } }); - wc_checkout_form.xhr = $.ajax({ + wc_checkout_form.controller = ajax({ type: 'POST', url: wc_checkout_params.wc_ajax_url.toString().replace( '%%endpoint%%', 'update_order_review' ), - data: data, + data: new URLSearchParams( data ).toString(), success: function( data ) { // Reload the page if requested @@ -404,8 +440,6 @@ jQuery( function( $ ) { // Check for error if ( data && 'failure' === data.result ) { - var $form = $( 'form.checkout' ); - // Remove notices from all sources $( '.woocommerce-error, .woocommerce-message' ).remove(); @@ -486,39 +520,37 @@ jQuery( function( $ ) { // Attach event to block reloading the page when the form has been submitted wc_checkout_form.attachUnloadEventsOnSubmit(); - // ajaxSetup is global, but we use it to ensure JSON is valid once returned. - $.ajaxSetup( { - dataFilter: function( raw_response, dataType ) { - // We only want to work with JSON - if ( 'json' !== dataType ) { - return raw_response; - } - - if ( wc_checkout_form.is_valid_json( raw_response ) ) { - return raw_response; - } else { - // Attempt to fix the malformed JSON - var maybe_valid_json = raw_response.match( /{"result.*}/ ); - - if ( null === maybe_valid_json ) { - console.log( 'Unable to fix malformed JSON' ); - } else if ( wc_checkout_form.is_valid_json( maybe_valid_json[0] ) ) { - console.log( 'Fixed malformed JSON. Original:' ); - console.log( raw_response ); - raw_response = maybe_valid_json[0]; - } else { - console.log( 'Unable to fix malformed JSON' ); - } - } - + // ajax.dataFilter affects all ajax() calls, but we use it to ensure JSON is valid once returned. + ajax.dataFilter = function( raw_response, dataType ) { + // We only want to work with JSON + if ( 'json' !== dataType ) { return raw_response; } - } ); - $.ajax({ + if ( wc_checkout_form.is_valid_json( raw_response ) ) { + return raw_response; + } else { + // Attempt to fix the malformed JSON + var maybe_valid_json = raw_response.match( /{"result.*}/ ); + + if ( null === maybe_valid_json ) { + console.log( 'Unable to fix malformed JSON' ); + } else if ( wc_checkout_form.is_valid_json( maybe_valid_json[0] ) ) { + console.log( 'Fixed malformed JSON. Original:' ); + console.log( raw_response ); + raw_response = maybe_valid_json[0]; + } else { + console.log( 'Unable to fix malformed JSON' ); + } + } + + return raw_response; + }; + + ajax({ type: 'POST', url: wc_checkout_params.checkout_url, - data: $form.serialize(), + data: new URLSearchParams( new FormData( $form[0] ) ).toString(), dataType: 'json', success: function( result ) { // Detach the unload handler that prevents a reload / redirect @@ -556,7 +588,7 @@ jQuery( function( $ ) { } } }, - error: function( jqXHR, textStatus, errorThrown ) { + error: function( responseText, textStatus, errorThrown ) { // Detach the unload handler that prevents a reload / redirect wc_checkout_form.detachUnloadEventsOnSubmit(); @@ -621,10 +653,10 @@ jQuery( function( $ ) { coupon_code: $form.find( 'input[name="coupon_code"]' ).val() }; - $.ajax({ + ajax({ type: 'POST', url: wc_checkout_params.wc_ajax_url.toString().replace( '%%endpoint%%', 'apply_coupon' ), - data: data, + data: new URLSearchParams( data ).toString(), success: function( code ) { $( '.woocommerce-error, .woocommerce-message' ).remove(); $form.removeClass( 'processing' ).unblock(); @@ -661,10 +693,10 @@ jQuery( function( $ ) { coupon: coupon }; - $.ajax({ + ajax({ type: 'POST', url: wc_checkout_params.wc_ajax_url.toString().replace( '%%endpoint%%', 'remove_coupon' ), - data: data, + data: new URLSearchParams( data ).toString(), success: function( code ) { $( '.woocommerce-error, .woocommerce-message' ).remove(); container.removeClass( 'processing' ).unblock(); @@ -679,10 +711,10 @@ jQuery( function( $ ) { $( 'form.checkout_coupon' ).find( 'input[name="coupon_code"]' ).val( '' ); } }, - error: function ( jqXHR ) { + error: function ( responseText ) { if ( wc_checkout_params.debug_mode ) { /* jshint devel: true */ - console.log( jqXHR.responseText ); + console.log( responseText ); } }, dataType: 'html' diff --git a/plugins/woocommerce/client/legacy/js/frontend/geolocation.js b/plugins/woocommerce/client/legacy/js/frontend/geolocation.js index e7029a3539d..29f8435f9df 100644 --- a/plugins/woocommerce/client/legacy/js/frontend/geolocation.js +++ b/plugins/woocommerce/client/legacy/js/frontend/geolocation.js @@ -127,7 +127,17 @@ jQuery( function( $ ) { // Get the current geo hash. If it doesn't exist, or if it doesn't match the current // page URL, perform a geolocation request. if ( ! get_geo_hash() || needs_refresh() ) { - $.ajax( $geolocate_customer ); + window.fetch( $geolocate_customer.url, { + method: $geolocate_customer.type, + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' } + } ) + .then( response => { + if ( !response.ok ) { + throw new Error( response.statusText ); + } + return response.json(); + } ) + .then( $geolocate_customer.success ); } // Page updates. From f601b0d8a51824b6b4d844dde21e328487a58a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9stor=20Soriano?= Date: Mon, 30 Jan 2023 21:04:45 +0100 Subject: [PATCH 120/228] [COT] Fix inappropriate deletion of order from posts with sync off (#36617) * Fix inappropriate deletion of order from posts with sync off When the orders table is authoritative and sync is off, deleting and order should not delete the order record from the posts table, this commit fixes that. Additionally, OrdersTableDataStore::delete will now trigger the woocommerce_delete_order action in all cases, even when the method is called while the posts table is authoritative. * Add changelog file * Fix: order items were not deleted * Improve comment in the 'delete' method * Use delete_items instead of a dedicated method for deleting order items --- .../fix-order_delete_from_posts_with_sync_off | 4 ++++ .../DataStores/Orders/OrdersTableDataStore.php | 14 +++++++------- .../Orders/OrdersTableDataStoreTests.php | 2 ++ 3 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-order_delete_from_posts_with_sync_off diff --git a/plugins/woocommerce/changelog/fix-order_delete_from_posts_with_sync_off b/plugins/woocommerce/changelog/fix-order_delete_from_posts_with_sync_off new file mode 100644 index 00000000000..29865c187c5 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-order_delete_from_posts_with_sync_off @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Don't delete order from posts table when deleted from orders table if the later is authoritative and sync is off diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index 0fd369cf83c..a78e7976eee 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -1763,18 +1763,18 @@ FROM $order_meta_table $this->upshift_child_orders( $order ); $this->delete_order_data_from_custom_order_tables( $order_id ); + $this->delete_items( $order ); $order->set_id( 0 ); - // If this datastore method is called while the posts table is authoritative, refrain from deleting post data. - if ( $order->get_data_store()->get_current_class_name() !== self::class ) { - return; + // Only delete post data if the posts table is authoritative and synchronization is enabled. + $data_synchronizer = wc_get_container()->get( DataSynchronizer::class ); + if ( $data_synchronizer->data_sync_is_enabled() && $order->get_data_store()->get_current_class_name() === self::class ) { + // Delete the associated post, which in turn deletes order items, etc. through {@see WC_Post_Data}. + // Once we stop creating posts for orders, we should do the cleanup here instead. + wp_delete_post( $order_id ); } - // Delete the associated post, which in turn deletes order items, etc. through {@see WC_Post_Data}. - // Once we stop creating posts for orders, we should do the cleanup here instead. - wp_delete_post( $order_id ); - do_action( 'woocommerce_delete_order', $order_id ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment } else { /** diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php index 2d7b5685c55..47538063371 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php @@ -8,6 +8,8 @@ use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableQuery; use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper; use Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait; +require_once __DIR__ . '/../../../../helpers/HPOSToggleTrait.php'; + /** * Class OrdersTableDataStoreTests. * From 83faf8d328ebb2d9d6e185f60d437392fd08221a Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Mon, 30 Jan 2023 13:07:05 -0800 Subject: [PATCH 121/228] Add unique sku option to error data when setting product sku (#36612) * Add unique sku option to error data when setting product sku * Add changelog entry * Add permalink template to the sku error data * Revert "Add permalink template to the sku error data" This reverts commit a49ee1d878642048b877d8eac2a1adb1829f054b. --- plugins/woocommerce/changelog/add-36589 | 4 ++++ .../includes/abstracts/abstract-wc-product.php | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/add-36589 diff --git a/plugins/woocommerce/changelog/add-36589 b/plugins/woocommerce/changelog/add-36589 new file mode 100644 index 00000000000..d1bd8db758d --- /dev/null +++ b/plugins/woocommerce/changelog/add-36589 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add unique sku option to error data when setting product sku diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-product.php b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php index 3e27d48c3d6..cd9385919e5 100644 --- a/plugins/woocommerce/includes/abstracts/abstract-wc-product.php +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php @@ -820,7 +820,15 @@ class WC_Product extends WC_Abstract_Legacy_Product { if ( $this->get_object_read() && ! empty( $sku ) && ! wc_product_has_unique_sku( $this->get_id(), $sku ) ) { $sku_found = wc_get_product_id_by_sku( $sku ); - $this->error( 'product_invalid_sku', __( 'Invalid or duplicated SKU.', 'woocommerce' ), 400, array( 'resource_id' => $sku_found ) ); + $this->error( + 'product_invalid_sku', + __( 'Invalid or duplicated SKU.', 'woocommerce' ), + 400, + array( + 'resource_id' => $sku_found, + 'unique_sku' => wc_product_generate_unique_sku( $this->get_id(), $sku ), + ) + ); } $this->set_prop( 'sku', $sku ); } From c0808862629d716b7a40d475d433423e6632b3a1 Mon Sep 17 00:00:00 2001 From: Joel Thiessen <444632+joelclimbsthings@users.noreply.github.com> Date: Tue, 31 Jan 2023 03:19:21 -0800 Subject: [PATCH 122/228] Implementation of separate variant slots via refactored components (#36646) * Adding contants, renaming files, wiring up shipping section with discrete slots for variants and non-variants * Refactoring pricing section to give discrete slots for variant and non-variant forms * Refactoring inventory fills to give discrete slots for variant and non-variant products * Adding additional slot to pricing -> taxes -> advanced * Renaming api fields component file * Refactoring images, details and attributes to match others * Ensure variant field IDs are distinct, organize all product field fill IDs * Adding changelog * Simplifying IDs used for slots * Minor refactor of pricing taxes class field * Give api fill exports their own registerPlugin scope * Refactoring WooProductFieldItem to generate unique IDs * Adding components changelog --- .../update-36610-support-variations-alternate | 4 + .../woo-product-field-item.tsx | 79 ++++++--- .../attributes-section-fills.tsx | 11 +- .../fills/attributes-section/index.ts | 1 + .../client/products/fills/constants.ts | 39 +++-- .../details-section/details-section-fills.tsx | 19 +- .../products/fills/details-section/index.ts | 1 + .../images-section/images-section-fills.tsx | 11 +- .../products/fills/images-section/index.ts | 1 + .../client/products/fills/index.ts | 11 +- .../products/fills/inventory-section/index.ts | 1 + .../inventory-section-fills.tsx | 62 +++---- .../products/fills/pricing-section/index.ts | 1 + .../pricing-field-taxes-class.tsx | 82 +++++---- .../pricing-section/pricing-section-fills.tsx | 60 ++++--- ...s.tsx => product-form-api-field-fills.tsx} | 0 .../products/fills/product-form-api-fills.tsx | 52 ++++++ ...tsx => product-form-api-section-fills.tsx} | 0 .../products/fills/product-form-fills.tsx | 164 ++++++++++++++---- .../products/fills/product-form-tab-fills.tsx | 116 ------------- .../fills/product-form-variation-fills.tsx | 125 +++++++++++++ .../product-form-variation-tab-fills.tsx | 95 ---------- .../products/fills/shipping-section/index.ts | 1 + .../shipping-section-fills.tsx | 64 +++---- .../update-36610-support-variations-alternate | 4 + 25 files changed, 542 insertions(+), 462 deletions(-) create mode 100644 packages/js/components/changelog/update-36610-support-variations-alternate rename plugins/woocommerce-admin/client/products/fills/{product-form-field-fills.tsx => product-form-api-field-fills.tsx} (100%) create mode 100644 plugins/woocommerce-admin/client/products/fills/product-form-api-fills.tsx rename plugins/woocommerce-admin/client/products/fills/{product-form-section-fills.tsx => product-form-api-section-fills.tsx} (100%) delete mode 100644 plugins/woocommerce-admin/client/products/fills/product-form-tab-fills.tsx create mode 100644 plugins/woocommerce-admin/client/products/fills/product-form-variation-fills.tsx delete mode 100644 plugins/woocommerce-admin/client/products/fills/product-form-variation-tab-fills.tsx create mode 100644 plugins/woocommerce/changelog/update-36610-support-variations-alternate diff --git a/packages/js/components/changelog/update-36610-support-variations-alternate b/packages/js/components/changelog/update-36610-support-variations-alternate new file mode 100644 index 00000000000..124a49aaa9f --- /dev/null +++ b/packages/js/components/changelog/update-36610-support-variations-alternate @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Updating WooProductFieldItem to uniquely generate IDs with different sections. diff --git a/packages/js/components/src/woo-product-field-item/woo-product-field-item.tsx b/packages/js/components/src/woo-product-field-item/woo-product-field-item.tsx index 78658ffb512..174b33928dd 100644 --- a/packages/js/components/src/woo-product-field-item/woo-product-field-item.tsx +++ b/packages/js/components/src/woo-product-field-item/woo-product-field-item.tsx @@ -22,41 +22,68 @@ type WooProductFieldSlotProps = { section: string; }; -export const WooProductFieldItem: React.FC< WooProductFieldItemProps > & { - Slot: React.FC< Slot.Props & WooProductFieldSlotProps >; -} = ( { children, sections, id } ) => { +type WooProductFieldFillProps = { + fieldName: string; + sectionName: string; + order: number; + children?: React.ReactNode; +}; + +const WooProductFieldFill: React.FC< WooProductFieldFillProps > = ( { + fieldName, + sectionName, + order, + children, +} ) => { const { registerFill, getFillHelpers } = useSlotContext(); + const fieldId = `product_field/${ sectionName }/${ fieldName }`; + useEffect( () => { - registerFill( id ); + registerFill( fieldId ); }, [] ); + return ( + + { ( fillProps: Fill.Props ) => + createOrderedChildren< + Fill.Props & + SlotContextHelpersType & { + sectionName: string; + }, + { _id: string } + >( + children, + order, + { + sectionName, + ...fillProps, + ...getFillHelpers(), + }, + { _id: fieldId } + ) + } + + ); +}; + +export const WooProductFieldItem: React.FC< WooProductFieldItemProps > & { + Slot: React.FC< Slot.Props & WooProductFieldSlotProps >; +} = ( { children, sections, id } ) => { return ( <> - { sections.map( ( { name: sectionName, order: sectionOrder } ) => ( - ( + - { ( fillProps: Fill.Props ) => { - return createOrderedChildren< - Fill.Props & - SlotContextHelpersType & { - sectionName: string; - }, - { _id: string } - >( - children, - sectionOrder || 20, - { - sectionName, - ...fillProps, - ...getFillHelpers(), - }, - { _id: id } - ); - } } - + { children } + ) ) } ); diff --git a/plugins/woocommerce-admin/client/products/fills/attributes-section/attributes-section-fills.tsx b/plugins/woocommerce-admin/client/products/fills/attributes-section/attributes-section-fills.tsx index 0bbf8bf42e9..270e316d061 100644 --- a/plugins/woocommerce-admin/client/products/fills/attributes-section/attributes-section-fills.tsx +++ b/plugins/woocommerce-admin/client/products/fills/attributes-section/attributes-section-fills.tsx @@ -8,7 +8,6 @@ import { __experimentalProductSectionLayout as ProductSectionLayout, Link, } from '@woocommerce/components'; -import { registerPlugin } from '@wordpress/plugins'; import { recordEvent } from '@woocommerce/tracks'; /** @@ -19,7 +18,7 @@ import { ATTRIBUTES_SECTION_ID, TAB_GENERAL_ID, PLUGIN_ID } from '../constants'; import './attributes-section.scss'; -const AttributesSection = () => ( +export const AttributesSectionFills = () => ( <> ( @@ -68,9 +67,3 @@ const AttributesSection = () => ( ); - -registerPlugin( 'wc-admin-product-editor-attributes-section', { - // @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. - scope: 'woocommerce-product-editor', - render: () => , -} ); diff --git a/plugins/woocommerce-admin/client/products/fills/attributes-section/index.ts b/plugins/woocommerce-admin/client/products/fills/attributes-section/index.ts index 655b5ecfa1c..57ba35a70ed 100644 --- a/plugins/woocommerce-admin/client/products/fills/attributes-section/index.ts +++ b/plugins/woocommerce-admin/client/products/fills/attributes-section/index.ts @@ -1 +1,2 @@ export * from './attributes-field'; +export * from './attributes-section-fills'; diff --git a/plugins/woocommerce-admin/client/products/fills/constants.ts b/plugins/woocommerce-admin/client/products/fills/constants.ts index f55290dd385..7dbd9b5e84d 100644 --- a/plugins/woocommerce-admin/client/products/fills/constants.ts +++ b/plugins/woocommerce-admin/client/products/fills/constants.ts @@ -1,17 +1,32 @@ -export const PRICING_SECTION_BASIC_ID = 'pricing/basic'; -export const PRICING_SECTION_TAXES_ID = 'pricing/taxes'; -export const DETAILS_SECTION_ID = 'general/details'; -export const INVENTORY_SECTION_ID = 'inventory/inventory'; -export const INVENTORY_SECTION_ADVANCED_ID = 'inventory/advanced'; -export const IMAGES_SECTION_ID = 'general/images'; -export const ATTRIBUTES_SECTION_ID = 'general/attributes'; -export const SHIPPING_SECTION_BASIC_ID = 'shipping/shipping'; -export const SHIPPING_SECTION_DIMENSIONS_ID = 'shipping/dimensions'; - -export const TAB_INVENTORY_ID = 'tab/inventory'; export const TAB_GENERAL_ID = 'tab/general'; -export const TAB_SHIPPING_ID = 'tab/shipping'; export const TAB_PRICING_ID = 'tab/pricing'; +export const TAB_INVENTORY_ID = 'tab/inventory'; +export const TAB_SHIPPING_ID = 'tab/shipping'; +export const TAB_OPTIONS_ID = 'tab/options'; + +export const VARIANT_TAB_GENERAL_ID = `variant/${ TAB_GENERAL_ID }`; +export const VARIANT_TAB_PRICING_ID = `variant/${ TAB_PRICING_ID }`; +export const VARIANT_TAB_INVENTORY_ID = `variant/${ TAB_INVENTORY_ID }`; +export const VARIANT_TAB_SHIPPING_ID = `variant/${ TAB_SHIPPING_ID }`; + +export const DETAILS_SECTION_ID = `${ TAB_GENERAL_ID }/details`; +export const IMAGES_SECTION_ID = `${ TAB_GENERAL_ID }/images`; +export const ATTRIBUTES_SECTION_ID = `${ TAB_GENERAL_ID }/attributes`; +export const PRICING_SECTION_BASIC_ID = `${ TAB_PRICING_ID }/basic`; +export const PRICING_SECTION_TAXES_ID = `${ TAB_PRICING_ID }/taxes`; +export const PRICING_SECTION_TAXES_ADVANCED_ID = `${ TAB_PRICING_ID }/taxes/advanced`; +export const INVENTORY_SECTION_ID = `${ TAB_INVENTORY_ID }/basic`; +export const INVENTORY_SECTION_ADVANCED_ID = `${ TAB_INVENTORY_ID }/advanced`; +export const SHIPPING_SECTION_BASIC_ID = `${ TAB_SHIPPING_ID }/basic`; +export const SHIPPING_SECTION_DIMENSIONS_ID = `${ TAB_SHIPPING_ID }/dimensions`; + +export const VARIANT_PRICING_SECTION_BASIC_ID = `variant/${ PRICING_SECTION_BASIC_ID }`; +export const VARIANT_PRICING_SECTION_TAXES_ID = `variant/${ PRICING_SECTION_TAXES_ID }`; +export const VARIANT_PRICING_SECTION_TAXES_ADVANCED_ID = `variant/${ PRICING_SECTION_TAXES_ADVANCED_ID }`; +export const VARIANT_INVENTORY_SECTION_ID = `variant/${ INVENTORY_SECTION_ID }`; +export const VARIANT_INVENTORY_SECTION_ADVANCED_ID = `variant/${ INVENTORY_SECTION_ADVANCED_ID }`; +export const VARIANT_SHIPPING_SECTION_BASIC_ID = `variant/${ SHIPPING_SECTION_BASIC_ID }`; +export const VARIANT_SHIPPING_SECTION_DIMENSIONS_ID = `variant/${ SHIPPING_SECTION_DIMENSIONS_ID }`; export const PLUGIN_ID = 'woocommerce'; export const PRODUCT_DETAILS_SLUG = 'product-details'; diff --git a/plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx b/plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx index be5c0bf11c4..bfcfd642117 100644 --- a/plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx +++ b/plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx @@ -7,7 +7,6 @@ import { __experimentalWooProductFieldItem as WooProductFieldItem, __experimentalProductFieldSection as ProductFieldSection, } from '@woocommerce/components'; -import { registerPlugin } from '@wordpress/plugins'; /** * Internal dependencies @@ -24,7 +23,7 @@ import { DETAILS_SECTION_ID, PLUGIN_ID, TAB_GENERAL_ID } from '../constants'; import './product-details-section.scss'; -const DetailsSection = () => ( +export const DetailsSectionFills = () => ( <> ( /> @@ -77,9 +76,3 @@ const DetailsSection = () => ( ); - -registerPlugin( 'wc-admin-product-editor-details-section', { - // @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. - scope: 'woocommerce-product-editor', - render: () => , -} ); diff --git a/plugins/woocommerce-admin/client/products/fills/details-section/index.ts b/plugins/woocommerce-admin/client/products/fills/details-section/index.ts index 1e342c4c1c1..606bad096cc 100644 --- a/plugins/woocommerce-admin/client/products/fills/details-section/index.ts +++ b/plugins/woocommerce-admin/client/products/fills/details-section/index.ts @@ -3,3 +3,4 @@ export * from './details-field-categories'; export * from './details-field-feature'; export * from './details-field-summary'; export * from './details-field-description'; +export * from './details-section-fills'; diff --git a/plugins/woocommerce-admin/client/products/fills/images-section/images-section-fills.tsx b/plugins/woocommerce-admin/client/products/fills/images-section/images-section-fills.tsx index 7c43e66447c..6a9b88df499 100644 --- a/plugins/woocommerce-admin/client/products/fills/images-section/images-section-fills.tsx +++ b/plugins/woocommerce-admin/client/products/fills/images-section/images-section-fills.tsx @@ -8,7 +8,6 @@ import { __experimentalProductFieldSection as ProductFieldSection, Link, } from '@woocommerce/components'; -import { registerPlugin } from '@wordpress/plugins'; import { recordEvent } from '@woocommerce/tracks'; /** @@ -19,7 +18,7 @@ import { IMAGES_SECTION_ID, TAB_GENERAL_ID, PLUGIN_ID } from '../constants'; import './images-section.scss'; -const ImagesSection = () => ( +export const ImagesSectionFills = () => ( <> ( /> @@ -64,9 +63,3 @@ const ImagesSection = () => ( ); - -registerPlugin( 'wc-admin-product-editor-images-section', { - // @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. - scope: 'woocommerce-product-editor', - render: () => , -} ); diff --git a/plugins/woocommerce-admin/client/products/fills/images-section/index.ts b/plugins/woocommerce-admin/client/products/fills/images-section/index.ts index 9d67f229c29..3d4700456a2 100644 --- a/plugins/woocommerce-admin/client/products/fills/images-section/index.ts +++ b/plugins/woocommerce-admin/client/products/fills/images-section/index.ts @@ -1 +1,2 @@ export * from './images-field-gallery'; +export * from './images-section-fills'; diff --git a/plugins/woocommerce-admin/client/products/fills/index.ts b/plugins/woocommerce-admin/client/products/fills/index.ts index b7f78b86fd8..d2227e3b666 100644 --- a/plugins/woocommerce-admin/client/products/fills/index.ts +++ b/plugins/woocommerce-admin/client/products/fills/index.ts @@ -1,13 +1,6 @@ /** * Internal dependencies */ +import './product-form-api-fills'; import './product-form-fills'; -import './product-form-tab-fills'; -import './product-form-variation-tab-fills'; - -export * from './shipping-section/shipping-section-fills'; -export * from './details-section/details-section-fills'; -export * from './images-section/images-section-fills'; -export * from './attributes-section/attributes-section-fills'; -export * from './inventory-section/inventory-section-fills'; -export * from './pricing-section/pricing-section-fills'; +import './product-form-variation-fills'; diff --git a/plugins/woocommerce-admin/client/products/fills/inventory-section/index.ts b/plugins/woocommerce-admin/client/products/fills/inventory-section/index.ts index 517338654ab..81713458934 100644 --- a/plugins/woocommerce-admin/client/products/fills/inventory-section/index.ts +++ b/plugins/woocommerce-admin/client/products/fills/inventory-section/index.ts @@ -4,3 +4,4 @@ export * from './inventory-field-stock-manual'; export * from './inventory-field-stock-manage'; export * from './inventory-field-stock-out'; export * from './inventory-field-stock-limit'; +export * from './inventory-section-fills'; diff --git a/plugins/woocommerce-admin/client/products/fills/inventory-section/inventory-section-fills.tsx b/plugins/woocommerce-admin/client/products/fills/inventory-section/inventory-section-fills.tsx index 0a466829147..efcce2629ee 100644 --- a/plugins/woocommerce-admin/client/products/fills/inventory-section/inventory-section-fills.tsx +++ b/plugins/woocommerce-admin/client/products/fills/inventory-section/inventory-section-fills.tsx @@ -11,7 +11,6 @@ import { CollapsibleContent, } from '@woocommerce/components'; import { Card, CardBody } from '@wordpress/components'; -import { registerPlugin } from '@wordpress/plugins'; import { recordEvent } from '@woocommerce/tracks'; import { getAdminLink } from '@woocommerce/settings'; import { Product } from '@woocommerce/data'; @@ -27,21 +26,26 @@ import { InventoryStockLimitField, InventoryStockOutField, } from './index'; -import { - INVENTORY_SECTION_ID, - INVENTORY_SECTION_ADVANCED_ID, - TAB_INVENTORY_ID, - PLUGIN_ID, -} from '../constants'; +import { PLUGIN_ID } from '../constants'; -const InventorySection = () => { +type InventorySectionFillsType = { + tabId: string; + basicSectionId: string; + advancedSectionId: string; +}; + +export const InventorySectionFills: React.FC< InventorySectionFillsType > = ( { + tabId, + basicSectionId, + advancedSectionId, +} ) => { const { values } = useFormContext< Product >(); return ( <> { @@ -90,15 +94,15 @@ const InventorySection = () => { @@ -106,16 +110,16 @@ const InventorySection = () => { { values.manage_stock ? ( ) : ( @@ -124,10 +128,8 @@ const InventorySection = () => { { values.manage_stock && ( @@ -135,10 +137,8 @@ const InventorySection = () => { ) } @@ -146,9 +146,3 @@ const InventorySection = () => { ); }; - -registerPlugin( 'wc-admin-product-editor-inventory-section', { - // @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. - scope: 'woocommerce-product-editor', - render: () => , -} ); diff --git a/plugins/woocommerce-admin/client/products/fills/pricing-section/index.ts b/plugins/woocommerce-admin/client/products/fills/pricing-section/index.ts index 8fa0169c0ff..1e03520d921 100644 --- a/plugins/woocommerce-admin/client/products/fills/pricing-section/index.ts +++ b/plugins/woocommerce-admin/client/products/fills/pricing-section/index.ts @@ -2,3 +2,4 @@ export * from './pricing-field-list'; export * from './pricing-field-sale'; export * from './pricing-field-taxes-charge'; export * from './pricing-field-taxes-class'; +export * from './pricing-section-fills'; diff --git a/plugins/woocommerce-admin/client/products/fills/pricing-section/pricing-field-taxes-class.tsx b/plugins/woocommerce-admin/client/products/fills/pricing-section/pricing-field-taxes-class.tsx index 3dc6f675298..7f1e91ba3d9 100644 --- a/plugins/woocommerce-admin/client/products/fills/pricing-section/pricing-field-taxes-class.tsx +++ b/plugins/woocommerce-admin/client/products/fills/pricing-section/pricing-field-taxes-class.tsx @@ -2,11 +2,7 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { - useFormContext, - CollapsibleContent, - Link, -} from '@woocommerce/components'; +import { useFormContext, Link } from '@woocommerce/components'; import { Product, EXPERIMENTAL_TAX_CLASSES_STORE_NAME, @@ -44,44 +40,44 @@ export const PricingTaxesClassField = () => { delete taxClassProps.checked; delete taxClassProps.value; + if ( isTaxClassesResolving || taxClasses.length <= 0 ) { + return null; + } + return ( - - { ! isTaxClassesResolving && taxClasses.length > 0 && ( - - { __( 'Tax class', 'woocommerce' ) } - - { interpolateComponents( { - mixedString: __( - 'Apply a tax rate if this product qualifies for tax reduction or exemption. {{link}}Learn more{{/link}}', - 'woocommerce' - ), - components: { - link: ( - - <> - - ), - }, - } ) } - - - } - options={ taxClasses.map( ( taxClass ) => ( { - label: taxClass.name, - value: - taxClass.slug === STANDARD_RATE_TAX_CLASS_SLUG - ? '' - : taxClass.slug, - } ) ) } - /> - ) } - + + { __( 'Tax class', 'woocommerce' ) } + + { interpolateComponents( { + mixedString: __( + 'Apply a tax rate if this product qualifies for tax reduction or exemption. {{link}}Learn more{{/link}}', + 'woocommerce' + ), + components: { + link: ( + + <> + + ), + }, + } ) } + + + } + options={ taxClasses.map( ( taxClass ) => ( { + label: taxClass.name, + value: + taxClass.slug === STANDARD_RATE_TAX_CLASS_SLUG + ? '' + : taxClass.slug, + } ) ) } + /> ); }; diff --git a/plugins/woocommerce-admin/client/products/fills/pricing-section/pricing-section-fills.tsx b/plugins/woocommerce-admin/client/products/fills/pricing-section/pricing-section-fills.tsx index 77cf32502b8..24d9a10d21a 100644 --- a/plugins/woocommerce-admin/client/products/fills/pricing-section/pricing-section-fills.tsx +++ b/plugins/woocommerce-admin/client/products/fills/pricing-section/pricing-section-fills.tsx @@ -8,8 +8,8 @@ import { __experimentalProductSectionLayout as ProductSectionLayout, Link, useFormContext, + CollapsibleContent, } from '@woocommerce/components'; -import { registerPlugin } from '@wordpress/plugins'; import { recordEvent } from '@woocommerce/tracks'; import { Product } from '@woocommerce/data'; import { useContext } from '@wordpress/element'; @@ -25,12 +25,7 @@ import { PricingTaxesChargeField, } from './index'; import { useProductHelper } from '../../use-product-helper'; -import { - PRICING_SECTION_BASIC_ID, - PRICING_SECTION_TAXES_ID, - TAB_PRICING_ID, - PLUGIN_ID, -} from '../constants'; +import { PLUGIN_ID } from '../constants'; import { CurrencyContext } from '../../../lib/currency-context'; import './pricing-section.scss'; @@ -43,7 +38,19 @@ export type CurrencyInputProps = { onKeyUp: ( event: React.KeyboardEvent< HTMLInputElement > ) => void; }; -const PricingSection = () => { +type PricingSectionFillsType = { + tabId: string; + basicSectionId: string; + taxesSectionId: string; + taxesAdvancedSectionId: string; +}; + +export const PricingSectionFills: React.FC< PricingSectionFillsType > = ( { + tabId, + basicSectionId, + taxesSectionId, + taxesAdvancedSectionId, +} ) => { const { setValues, values } = useFormContext< Product >(); const { sanitizePrice } = useProductHelper(); @@ -96,8 +103,8 @@ const PricingSection = () => { return ( <> { + + + @@ -174,9 +188,3 @@ const PricingSection = () => { ); }; - -registerPlugin( 'wc-admin-product-editor-pricing-section', { - // @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. - scope: 'woocommerce-product-editor', - render: () => , -} ); diff --git a/plugins/woocommerce-admin/client/products/fills/product-form-field-fills.tsx b/plugins/woocommerce-admin/client/products/fills/product-form-api-field-fills.tsx similarity index 100% rename from plugins/woocommerce-admin/client/products/fills/product-form-field-fills.tsx rename to plugins/woocommerce-admin/client/products/fills/product-form-api-field-fills.tsx diff --git a/plugins/woocommerce-admin/client/products/fills/product-form-api-fills.tsx b/plugins/woocommerce-admin/client/products/fills/product-form-api-fills.tsx new file mode 100644 index 00000000000..9f5bd8483e5 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fills/product-form-api-fills.tsx @@ -0,0 +1,52 @@ +/** + * External dependencies + */ +import { registerPlugin } from '@wordpress/plugins'; +import { useSelect, resolveSelect } from '@wordpress/data'; +import { + EXPERIMENTAL_PRODUCT_FORM_STORE_NAME, + WCDataSelector, +} from '@woocommerce/data'; +import { registerCoreProductFields } from '@woocommerce/components'; + +registerCoreProductFields(); + +/** + * Internal dependencies + */ +import { Fields } from './product-form-api-field-fills'; +import { Sections } from './product-form-api-section-fills'; + +const Form = () => { + const { formData } = useSelect( ( select: WCDataSelector ) => { + return { + formData: select( + EXPERIMENTAL_PRODUCT_FORM_STORE_NAME + ).getProductForm(), + }; + } ); + + return ( + <> + { formData && ( + <> + + + + ) } + + ); +}; + +/** + * Preloading product form data, as product pages are waiting on this to be resolved. + * The above Form component won't get rendered until the getProductForm is resolved. + */ +resolveSelect( EXPERIMENTAL_PRODUCT_FORM_STORE_NAME ).getProductForm(); +registerPlugin( 'wc-admin-product-editor-api-form-fills', { + // @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. + scope: 'woocommerce-product-editor', + render: () => { + return
; + }, +} ); diff --git a/plugins/woocommerce-admin/client/products/fills/product-form-section-fills.tsx b/plugins/woocommerce-admin/client/products/fills/product-form-api-section-fills.tsx similarity index 100% rename from plugins/woocommerce-admin/client/products/fills/product-form-section-fills.tsx rename to plugins/woocommerce-admin/client/products/fills/product-form-api-section-fills.tsx diff --git a/plugins/woocommerce-admin/client/products/fills/product-form-fills.tsx b/plugins/woocommerce-admin/client/products/fills/product-form-fills.tsx index 0710edfd2fd..7d52e81477b 100644 --- a/plugins/woocommerce-admin/client/products/fills/product-form-fills.tsx +++ b/plugins/woocommerce-admin/client/products/fills/product-form-fills.tsx @@ -1,52 +1,152 @@ /** * External dependencies */ +import { __ } from '@wordpress/i18n'; import { registerPlugin } from '@wordpress/plugins'; -import { useSelect, resolveSelect } from '@wordpress/data'; import { - EXPERIMENTAL_PRODUCT_FORM_STORE_NAME, - WCDataSelector, -} from '@woocommerce/data'; -import { registerCoreProductFields } from '@woocommerce/components'; - -registerCoreProductFields(); + __experimentalWooProductTabItem as WooProductTabItem, + __experimentalWooProductSectionItem as WooProductSectionItem, + useFormContext, +} from '@woocommerce/components'; +import { Product } from '@woocommerce/data'; +import { useMemo } from '@wordpress/element'; /** * Internal dependencies */ -import { Fields } from './product-form-field-fills'; -import { Sections } from './product-form-section-fills'; - -const Form = () => { - const { formData } = useSelect( ( select: WCDataSelector ) => { - return { - formData: select( - EXPERIMENTAL_PRODUCT_FORM_STORE_NAME - ).getProductForm(), - }; - } ); +import { DetailsSectionFills } from './details-section'; +import { ShippingSectionFills } from './shipping-section'; +import { PricingSectionFills } from './pricing-section'; +import { InventorySectionFills } from './inventory-section'; +import { AttributesSectionFills } from './attributes-section'; +import { ImagesSectionFills } from './images-section'; +import { OptionsSection } from '../sections/options-section'; +import { ProductVariationsSection } from '../sections/product-variations-section'; +import { + TAB_GENERAL_ID, + TAB_SHIPPING_ID, + TAB_INVENTORY_ID, + TAB_PRICING_ID, + TAB_OPTIONS_ID, + SHIPPING_SECTION_BASIC_ID, + SHIPPING_SECTION_DIMENSIONS_ID, + PRICING_SECTION_BASIC_ID, + PRICING_SECTION_TAXES_ID, + PRICING_SECTION_TAXES_ADVANCED_ID, + INVENTORY_SECTION_ID, + INVENTORY_SECTION_ADVANCED_ID, + PLUGIN_ID, +} from './constants'; +const Tabs = () => { + const { values: product } = useFormContext< Product >(); + const tabPropData = useMemo( + () => ( { + general: { + name: 'general', + title: __( 'General', 'woocommerce' ), + }, + pricing: { + name: 'pricing', + title: __( 'Pricing', 'woocommerce' ), + disabled: !! product?.variations?.length, + }, + inventory: { + name: 'inventory', + title: __( 'Inventory', 'woocommerce' ), + disabled: !! product?.variations?.length, + }, + shipping: { + name: 'shipping', + title: __( 'Shipping', 'woocommerce' ), + disabled: !! product?.variations?.length, + }, + options: { + name: 'options', + title: __( 'Options', 'woocommerce' ), + }, + } ), + [ product.variations ] + ); return ( <> - { formData && ( - <> - - - - ) } + + + + + + + + + + + + + { window.wcAdminFeatures[ 'product-variation-management' ] ? ( + + <> + + + + + ) : null } ); }; -/** - * Preloading product form data, as product pages are waiting on this to be resolved. - * The above Form component won't get rendered until the getProductForm is resolved. - */ -resolveSelect( EXPERIMENTAL_PRODUCT_FORM_STORE_NAME ).getProductForm(); registerPlugin( 'wc-admin-product-editor-form-fills', { // @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. scope: 'woocommerce-product-editor', - render: () => { - return ; - }, + render: () => ( + <> + + + + + + + + + ), } ); diff --git a/plugins/woocommerce-admin/client/products/fills/product-form-tab-fills.tsx b/plugins/woocommerce-admin/client/products/fills/product-form-tab-fills.tsx deleted file mode 100644 index 5700e213fb8..00000000000 --- a/plugins/woocommerce-admin/client/products/fills/product-form-tab-fills.tsx +++ /dev/null @@ -1,116 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { registerPlugin } from '@wordpress/plugins'; -import { - __experimentalWooProductTabItem as WooProductTabItem, - __experimentalWooProductSectionItem as WooProductSectionItem, - useFormContext, -} from '@woocommerce/components'; -import { Product } from '@woocommerce/data'; -import { useMemo } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { OptionsSection } from '../sections/options-section'; -import { ProductVariationsSection } from '../sections/product-variations-section'; -import { - TAB_GENERAL_ID, - TAB_SHIPPING_ID, - TAB_INVENTORY_ID, - TAB_PRICING_ID, -} from './constants'; - -const Tabs = () => { - const { values: product } = useFormContext< Product >(); - const tabPropData = useMemo( - () => ( { - general: { - name: 'general', - title: __( 'General', 'woocommerce' ), - }, - pricing: { - name: 'pricing', - title: __( 'Pricing', 'woocommerce' ), - disabled: !! product?.variations?.length, - }, - inventory: { - name: 'inventory', - title: __( 'Inventory', 'woocommerce' ), - disabled: !! product?.variations?.length, - }, - shipping: { - name: 'shipping', - title: __( 'Shipping', 'woocommerce' ), - disabled: !! product?.variations?.length, - }, - options: { - name: 'options', - title: __( 'Options', 'woocommerce' ), - }, - } ), - [ product.variations ] - ); - return ( - <> - - - - - - - - - - - - - { window.wcAdminFeatures[ 'product-variation-management' ] ? ( - - <> - - - - - ) : null } - - ); -}; - -registerPlugin( 'wc-admin-product-editor-form-tab-fills', { - // @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. - scope: 'woocommerce-product-editor', - render: () => { - return ; - }, -} ); diff --git a/plugins/woocommerce-admin/client/products/fills/product-form-variation-fills.tsx b/plugins/woocommerce-admin/client/products/fills/product-form-variation-fills.tsx new file mode 100644 index 00000000000..8b75c058295 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fills/product-form-variation-fills.tsx @@ -0,0 +1,125 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { registerPlugin } from '@wordpress/plugins'; +import { + __experimentalWooProductTabItem as WooProductTabItem, + __experimentalWooProductSectionItem as WooProductSectionItem, +} from '@woocommerce/components'; +import { PartialProduct } from '@woocommerce/data'; + +/** + * Internal dependencies + */ +import { ProductVariationDetailsSection } from '../sections/product-variation-details-section'; +import { + VARIANT_TAB_GENERAL_ID, + VARIANT_TAB_SHIPPING_ID, + VARIANT_TAB_PRICING_ID, + VARIANT_TAB_INVENTORY_ID, + VARIANT_SHIPPING_SECTION_BASIC_ID, + VARIANT_SHIPPING_SECTION_DIMENSIONS_ID, + VARIANT_PRICING_SECTION_BASIC_ID, + VARIANT_PRICING_SECTION_TAXES_ID, + VARIANT_PRICING_SECTION_TAXES_ADVANCED_ID, + VARIANT_INVENTORY_SECTION_ID, + VARIANT_INVENTORY_SECTION_ADVANCED_ID, + PLUGIN_ID, +} from './constants'; +import { ShippingSectionFills } from './shipping-section'; +import { PricingSectionFills } from './pricing-section'; +import { InventorySectionFills } from './inventory-section'; + +const tabPropData = { + general: { + name: 'general', + title: __( 'General', 'woocommerce' ), + }, + pricing: { + name: 'pricing', + title: __( 'Pricing', 'woocommerce' ), + }, + inventory: { + name: 'inventory', + title: __( 'Inventory', 'woocommerce' ), + }, + shipping: { + name: 'shipping', + title: __( 'Shipping', 'woocommerce' ), + }, + options: { + name: 'options', + title: __( 'Options', 'woocommerce' ), + }, +}; + +const Tabs = () => ( + <> + + + + + + + + + + + { ( { product }: { product: PartialProduct } ) => ( + + ) } + + +); + +registerPlugin( 'wc-admin-product-editor-form-variation-fills', { + // @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. + scope: 'woocommerce-product-editor', + render: () => ( + <> + + + + + + ), +} ); diff --git a/plugins/woocommerce-admin/client/products/fills/product-form-variation-tab-fills.tsx b/plugins/woocommerce-admin/client/products/fills/product-form-variation-tab-fills.tsx deleted file mode 100644 index 0c7048510e0..00000000000 --- a/plugins/woocommerce-admin/client/products/fills/product-form-variation-tab-fills.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { registerPlugin } from '@wordpress/plugins'; -import { - __experimentalWooProductTabItem as WooProductTabItem, - __experimentalWooProductSectionItem as WooProductSectionItem, -} from '@woocommerce/components'; -import { PartialProduct } from '@woocommerce/data'; - -/** - * Internal dependencies - */ -import { ProductVariationDetailsSection } from '../sections/product-variation-details-section'; -import { TAB_INVENTORY_ID, TAB_SHIPPING_ID, TAB_PRICING_ID } from './constants'; - -const tabPropData = { - general: { - name: 'general', - title: __( 'General', 'woocommerce' ), - }, - pricing: { - name: 'pricing', - title: __( 'Pricing', 'woocommerce' ), - }, - inventory: { - name: 'inventory', - title: __( 'Inventory', 'woocommerce' ), - }, - shipping: { - name: 'shipping', - title: __( 'Shipping', 'woocommerce' ), - }, - options: { - name: 'options', - title: __( 'Options', 'woocommerce' ), - }, -}; - -const Tabs = () => { - return ( - <> - - - - - - - - - - - { ( { product }: { product: PartialProduct } ) => ( - - ) } - - - ); -}; - -/** - * Preloading product form data, as product pages are waiting on this to be resolved. - * The above Form component won't get rendered until the getProductForm is resolved. - */ -registerPlugin( 'wc-admin-product-editor-form-variation-tab-fills', { - // @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. - scope: 'woocommerce-product-editor', - render: () => { - return ; - }, -} ); diff --git a/plugins/woocommerce-admin/client/products/fills/shipping-section/index.ts b/plugins/woocommerce-admin/client/products/fills/shipping-section/index.ts index 3ad9082f302..488a5d75e26 100644 --- a/plugins/woocommerce-admin/client/products/fills/shipping-section/index.ts +++ b/plugins/woocommerce-admin/client/products/fills/shipping-section/index.ts @@ -4,3 +4,4 @@ export * from './shipping-field-dimensions-length'; export * from './shipping-field-dimensions-height'; export * from './shipping-field-dimensions-weight'; export * from './types'; +export * from './shipping-section-fills'; diff --git a/plugins/woocommerce-admin/client/products/fills/shipping-section/shipping-section-fills.tsx b/plugins/woocommerce-admin/client/products/fills/shipping-section/shipping-section-fills.tsx index b45269fa08c..27e8916a810 100644 --- a/plugins/woocommerce-admin/client/products/fills/shipping-section/shipping-section-fills.tsx +++ b/plugins/woocommerce-admin/client/products/fills/shipping-section/shipping-section-fills.tsx @@ -7,7 +7,6 @@ import { __experimentalWooProductFieldItem as WooProductFieldItem, __experimentalProductSectionLayout as ProductSectionLayout, } from '@woocommerce/components'; -import { registerPlugin } from '@wordpress/plugins'; import { PartialProduct, OPTIONS_STORE_NAME } from '@woocommerce/data'; import { useSelect } from '@wordpress/data'; import { useState } from '@wordpress/element'; @@ -26,12 +25,7 @@ import { DimensionPropsType, ShippingDimensionsPropsType, } from './index'; -import { - PLUGIN_ID, - SHIPPING_SECTION_BASIC_ID, - SHIPPING_SECTION_DIMENSIONS_ID, - TAB_SHIPPING_ID, -} from '../constants'; +import { PLUGIN_ID } from '../constants'; import { ShippingDimensionsImage, ShippingDimensionsImageProps, @@ -40,7 +34,17 @@ import { useProductHelper } from '../../use-product-helper'; import './shipping-section.scss'; -const ShippingSection = () => { +type ShippingSectionProps = { + basicSectionId: string; + dimensionsSectionId: string; + tabId: string; +}; + +export const ShippingSectionFills: React.FC< ShippingSectionProps > = ( { + basicSectionId, + dimensionsSectionId, + tabId, +} ) => { const [ highlightSide, setHighlightSide ] = useState< ShippingDimensionsImageProps[ 'highlight' ] >(); const { parseNumber } = useProductHelper(); @@ -73,8 +77,8 @@ const ShippingSection = () => { return ( <> { @@ -104,9 +108,7 @@ const ShippingSection = () => {
{ hasResolvedUnits && ( { { ( { product }: ProductShippingSectionPropsType ) => ( @@ -137,10 +139,8 @@ const ShippingSection = () => { ) } { ( { ...props }: ShippingDimensionsPropsType ) => ( @@ -148,10 +148,8 @@ const ShippingSection = () => { ) } { ( { ...props }: ShippingDimensionsPropsType ) => ( @@ -159,10 +157,8 @@ const ShippingSection = () => { ) } { ( { ...props }: ShippingDimensionsPropsType ) => ( @@ -170,10 +166,8 @@ const ShippingSection = () => { ) } @@ -181,9 +175,3 @@ const ShippingSection = () => { ); }; - -registerPlugin( 'wc-admin-product-editor-shipping-section', { - // @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. - scope: 'woocommerce-product-editor', - render: () => , -} ); diff --git a/plugins/woocommerce/changelog/update-36610-support-variations-alternate b/plugins/woocommerce/changelog/update-36610-support-variations-alternate new file mode 100644 index 00000000000..5fff7ed3674 --- /dev/null +++ b/plugins/woocommerce/changelog/update-36610-support-variations-alternate @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Refactor slot fills to ensure variant fills have distinct slots. From 5f0572664f7041afe8592c904c40d73aa7e8c70a Mon Sep 17 00:00:00 2001 From: louwie17 Date: Tue, 31 Jan 2023 09:38:28 -0400 Subject: [PATCH 123/228] Add new `@woocommerce/product-editor` JS package (#36600) * Bootstrap product-editor package * Move product section components over to product editor package * Add changelogs * Remove unused import leftover from rebase --- .../add-36355_new_product_editor_package | 4 + packages/js/components/src/index.ts | 4 - packages/js/components/src/style.scss | 1 - .../assets/packages.js | 1 + .../add-36355_new_product_editor_package | 4 + packages/js/product-editor/.npmrc | 1 + packages/js/product-editor/README.md | 11 + .../add-36355_new_product_editor_package | 4 + packages/js/product-editor/composer.json | 32 ++ packages/js/product-editor/composer.lock | 483 ++++++++++++++++++ packages/js/product-editor/package.json | 64 +++ packages/js/product-editor/src/index.ts | 4 + .../src/product-section-layout/index.ts | 0 .../product-field-section.tsx | 2 +- .../product-section-layout.tsx | 6 +- .../src/product-section-layout/style.scss | 0 packages/js/product-editor/src/style.scss | 1 + packages/js/product-editor/tsconfig-cjs.json | 6 + packages/js/product-editor/tsconfig.json | 10 + packages/js/product-editor/webpack.config.js | 18 + .../attributes-section-fills.tsx | 2 +- .../details-section/details-section-fills.tsx | 2 +- .../images-section/images-section-fills.tsx | 2 +- .../inventory-section-fills.tsx | 2 +- .../pricing-section/pricing-section-fills.tsx | 2 +- .../fills/product-form-api-section-fills.tsx | 7 +- .../shipping-section-fills.tsx | 2 +- plugins/woocommerce-admin/package.json | 1 + plugins/woocommerce-admin/webpack.config.js | 1 + .../add-36355_new_product_editor_package | 4 + .../src/Internal/Admin/WCAdminAssets.php | 12 +- pnpm-lock.yaml | 227 ++++++-- 32 files changed, 852 insertions(+), 68 deletions(-) create mode 100644 packages/js/components/changelog/add-36355_new_product_editor_package create mode 100644 packages/js/dependency-extraction-webpack-plugin/changelog/add-36355_new_product_editor_package create mode 100644 packages/js/product-editor/.npmrc create mode 100644 packages/js/product-editor/README.md create mode 100644 packages/js/product-editor/changelog/add-36355_new_product_editor_package create mode 100644 packages/js/product-editor/composer.json create mode 100644 packages/js/product-editor/composer.lock create mode 100644 packages/js/product-editor/package.json create mode 100644 packages/js/product-editor/src/index.ts rename packages/js/{components => product-editor}/src/product-section-layout/index.ts (100%) rename packages/js/{components => product-editor}/src/product-section-layout/product-field-section.tsx (88%) rename packages/js/{components => product-editor}/src/product-section-layout/product-section-layout.tsx (89%) rename packages/js/{components => product-editor}/src/product-section-layout/style.scss (100%) create mode 100644 packages/js/product-editor/src/style.scss create mode 100644 packages/js/product-editor/tsconfig-cjs.json create mode 100644 packages/js/product-editor/tsconfig.json create mode 100644 packages/js/product-editor/webpack.config.js create mode 100644 plugins/woocommerce/changelog/add-36355_new_product_editor_package diff --git a/packages/js/components/changelog/add-36355_new_product_editor_package b/packages/js/components/changelog/add-36355_new_product_editor_package new file mode 100644 index 00000000000..19797698691 --- /dev/null +++ b/packages/js/components/changelog/add-36355_new_product_editor_package @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Move experimental product section components to @woocommerce/product-editor package. diff --git a/packages/js/components/src/index.ts b/packages/js/components/src/index.ts index f038899e38c..38816ca6c0f 100644 --- a/packages/js/components/src/index.ts +++ b/packages/js/components/src/index.ts @@ -88,10 +88,6 @@ export { createOrderedChildren, sortFillsByOrder } from './utils'; export { WooProductFieldItem as __experimentalWooProductFieldItem } from './woo-product-field-item'; export { WooProductSectionItem as __experimentalWooProductSectionItem } from './woo-product-section-item'; export { WooProductTabItem as __experimentalWooProductTabItem } from './woo-product-tab-item'; -export { - ProductSectionLayout as __experimentalProductSectionLayout, - ProductFieldSection as __experimentalProductFieldSection, -} from './product-section-layout'; export * from './product-fields'; export { SlotContextProvider, diff --git a/packages/js/components/src/style.scss b/packages/js/components/src/style.scss index 1f2c3be423e..a38163a2a32 100644 --- a/packages/js/components/src/style.scss +++ b/packages/js/components/src/style.scss @@ -56,5 +56,4 @@ @import 'tour-kit/style.scss'; @import 'collapsible-content/style.scss'; @import 'form/style.scss'; -@import 'product-section-layout/style.scss'; @import 'experimental-tree-control/tree.scss'; diff --git a/packages/js/dependency-extraction-webpack-plugin/assets/packages.js b/packages/js/dependency-extraction-webpack-plugin/assets/packages.js index 7f053d7acf2..42df69e8535 100644 --- a/packages/js/dependency-extraction-webpack-plugin/assets/packages.js +++ b/packages/js/dependency-extraction-webpack-plugin/assets/packages.js @@ -14,6 +14,7 @@ module.exports = [ '@woocommerce/navigation', '@woocommerce/notices', '@woocommerce/number', + '@woocommerce/product-editor', '@woocommerce/tracks', // wc-blocks packages '@woocommerce/blocks-checkout', diff --git a/packages/js/dependency-extraction-webpack-plugin/changelog/add-36355_new_product_editor_package b/packages/js/dependency-extraction-webpack-plugin/changelog/add-36355_new_product_editor_package new file mode 100644 index 00000000000..eb22e8071c4 --- /dev/null +++ b/packages/js/dependency-extraction-webpack-plugin/changelog/add-36355_new_product_editor_package @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add @woocommerce/product-editor package to the packages list. diff --git a/packages/js/product-editor/.npmrc b/packages/js/product-editor/.npmrc new file mode 100644 index 00000000000..43c97e719a5 --- /dev/null +++ b/packages/js/product-editor/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/js/product-editor/README.md b/packages/js/product-editor/README.md new file mode 100644 index 00000000000..ecbb246c040 --- /dev/null +++ b/packages/js/product-editor/README.md @@ -0,0 +1,11 @@ +# Product Editor + +A collection of WooCommerce Admin product editor components and utilities. + +## Installation + +Install the module + +```bash +pnpm install @woocommerce/product-editor --save +``` diff --git a/packages/js/product-editor/changelog/add-36355_new_product_editor_package b/packages/js/product-editor/changelog/add-36355_new_product_editor_package new file mode 100644 index 00000000000..e2ad0fa7059 --- /dev/null +++ b/packages/js/product-editor/changelog/add-36355_new_product_editor_package @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Adding initial build/package files for brand new package: @woocommere/product-editor. diff --git a/packages/js/product-editor/composer.json b/packages/js/product-editor/composer.json new file mode 100644 index 00000000000..84a7b3a254f --- /dev/null +++ b/packages/js/product-editor/composer.json @@ -0,0 +1,32 @@ +{ + "name": "woocommerce/product-editor", + "description": "WooCommerce Admin product editor component library", + "type": "library", + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { + "automattic/jetpack-changelogger": "3.3.0" + }, + "config": { + "platform": { + "php": "7.2" + } + }, + "extra": { + "changelogger": { + "formatter": { + "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", + "add": "Adds functionality", + "update": "Update existing functionality", + "dev": "Development related task", + "tweak": "A minor adjustment to the codebase", + "performance": "Address performance issues", + "enhancement": "Improve existing functionality" + }, + "changelog": "CHANGELOG.md" + } + } +} diff --git a/packages/js/product-editor/composer.lock b/packages/js/product-editor/composer.lock new file mode 100644 index 00000000000..5aa2538970b --- /dev/null +++ b/packages/js/product-editor/composer.lock @@ -0,0 +1,483 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "36c31d66fa022c27e5bb2bf5db177a6b", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", + "reference": "8f63c829b8d1b0d7b1d5de93510d78523ed18959" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/8f63c829b8d1b0d7b1d5de93510d78523ed18959", + "reference": "8f63c829b8d1b0d7b1d5de93510d78523ed18959", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "symfony/console": "^3.4 || ^5.2 || ^6.0", + "symfony/process": "^3.4 || ^5.2 || ^6.0", + "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "yoast/phpunit-polyfills": "1.0.4" + }, + "bin": [ + "bin/changelogger" + ], + "type": "project", + "extra": { + "autotagger": true, + "branch-alias": { + "dev-trunk": "3.3.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { + "::VERSION": "src/Application.php" + }, + "changelogger": { + "link-template": "https://github.com/Automattic/jetpack-changelogger/compare/${old}...${new}" + } + }, + "autoload": { + "psr-4": { + "Automattic\\Jetpack\\Changelog\\": "lib", + "Automattic\\Jetpack\\Changelogger\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { + "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.3.0" + }, + "time": "2022-12-26T13:49:01+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + }, + { + "name": "symfony/console", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "a10b1da6fc93080c180bba7219b5ff5b7518fe81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/a10b1da6fc93080c180bba7219b5ff5b7518fe81", + "reference": "a10b1da6fc93080c180bba7219b5ff5b7518fe81", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/debug": "~2.8|~3.0|~4.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/process": "<3.3" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.3|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.3|~4.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/console/tree/3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T10:57:07+00:00" + }, + { + "name": "symfony/debug", + "version": "4.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "1a692492190773c5310bc7877cb590c04c2f05be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/1a692492190773c5310bc7877cb590c04c2f05be", + "reference": "1a692492190773c5310bc7877cb590c04c2f05be", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "symfony/http-kernel": "<3.4" + }, + "require-dev": { + "symfony/http-kernel": "^3.4|^4.0|^5.0" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/debug/tree/v4.4.44" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "abandoned": "symfony/error-handler", + "time": "2022-07-28T16:29:46+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/process", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/b8648cf1d5af12a44a51d07ef9bf980921f15fca", + "reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T10:57:07+00:00" + }, + { + "name": "wikimedia/at-ease", + "version": "v2.0.0", + "source": { + "type": "git", + "url": "https://github.com/wikimedia/at-ease.git", + "reference": "013ac61929797839c80a111a3f1a4710d8248e7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wikimedia/at-ease/zipball/013ac61929797839c80a111a3f1a4710d8248e7a", + "reference": "013ac61929797839c80a111a3f1a4710d8248e7a", + "shasum": "" + }, + "require": { + "php": ">=5.6.99" + }, + "require-dev": { + "jakub-onderka/php-console-highlighter": "0.3.2", + "jakub-onderka/php-parallel-lint": "1.0.0", + "mediawiki/mediawiki-codesniffer": "22.0.0", + "mediawiki/minus-x": "0.3.1", + "ockcyp/covers-validator": "0.5.1 || 0.6.1", + "phpunit/phpunit": "4.8.36 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/Wikimedia/Functions.php" + ], + "psr-4": { + "Wikimedia\\AtEase\\": "src/Wikimedia/AtEase/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Tim Starling", + "email": "tstarling@wikimedia.org" + }, + { + "name": "MediaWiki developers", + "email": "wikitech-l@lists.wikimedia.org" + } + ], + "description": "Safe replacement to @ for suppressing warnings.", + "homepage": "https://www.mediawiki.org/wiki/at-ease", + "support": { + "source": "https://github.com/wikimedia/at-ease/tree/master" + }, + "time": "2018-10-10T15:39:06+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "platform-overrides": { + "php": "7.2" + }, + "plugin-api-version": "2.3.0" +} diff --git a/packages/js/product-editor/package.json b/packages/js/product-editor/package.json new file mode 100644 index 00000000000..7e22fc72096 --- /dev/null +++ b/packages/js/product-editor/package.json @@ -0,0 +1,64 @@ +{ + "name": "@woocommerce/product-editor", + "version": "1.0.0-beta.0", + "description": "React components for the WooCommerce admin product editor.", + "author": "Automattic", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "woocommerce" + ], + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/product-editor/README.md", + "repository": { + "type": "git", + "url": "https://github.com/woocommerce/woocommerce.git" + }, + "bugs": { + "url": "https://github.com/woocommerce/woocommerce/issues" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "types": "build-types", + "react-native": "src/index", + "sideEffects": [ + "build-style/**", + "src/**/*.scss" + ], + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@woocommerce/components": "workspace:*", + "@wordpress/components": "^19.5.0", + "@wordpress/element": "^4.1.1" + }, + "devDependencies": { + "@types/wordpress__components": "^19.10.1", + "@woocommerce/eslint-plugin": "workspace:*", + "@woocommerce/internal-style-build": "workspace:*", + "@wordpress/browserslist-config": "^4.1.1", + "css-loader": "^3.6.0", + "eslint": "^8.12.0", + "jest": "^27.5.1", + "jest-cli": "^27.5.1", + "postcss-loader": "^3.0.0", + "sass-loader": "^10.2.1", + "ts-jest": "^27.1.3", + "typescript": "^4.8.3", + "webpack": "^5.70.0", + "webpack-cli": "^3.3.12" + }, + "scripts": { + "turbo:build": "pnpm run build:js && pnpm run build:css", + "prepare": "composer install", + "changelog": "composer exec -- changelogger", + "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", + "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", + "lint": "eslint src", + "build:js": "tsc --build ./tsconfig.json ./tsconfig-cjs.json", + "build:css": "webpack", + "start": "concurrently \"tsc --build --watch\" \"webpack --watch\"", + "prepack": "pnpm run clean && pnpm run build", + "lint:fix": "eslint src --fix" + } +} diff --git a/packages/js/product-editor/src/index.ts b/packages/js/product-editor/src/index.ts new file mode 100644 index 00000000000..e38787183bf --- /dev/null +++ b/packages/js/product-editor/src/index.ts @@ -0,0 +1,4 @@ +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/product-editor/src/product-section-layout/index.ts similarity index 100% rename from packages/js/components/src/product-section-layout/index.ts rename to packages/js/product-editor/src/product-section-layout/index.ts diff --git a/packages/js/components/src/product-section-layout/product-field-section.tsx b/packages/js/product-editor/src/product-section-layout/product-field-section.tsx similarity index 88% rename from packages/js/components/src/product-section-layout/product-field-section.tsx rename to packages/js/product-editor/src/product-section-layout/product-field-section.tsx index 5ae1d11b36c..f0dfb0133d4 100644 --- a/packages/js/components/src/product-section-layout/product-field-section.tsx +++ b/packages/js/product-editor/src/product-section-layout/product-field-section.tsx @@ -3,12 +3,12 @@ */ import { createElement } from '@wordpress/element'; import { Card, CardBody } from '@wordpress/components'; +import { __experimentalWooProductFieldItem as WooProductFieldItem } from '@woocommerce/components'; /** * Internal dependencies */ import { ProductSectionLayout } from './product-section-layout'; -import { WooProductFieldItem } from '../woo-product-field-item'; type ProductFieldSectionProps = { id: string; diff --git a/packages/js/components/src/product-section-layout/product-section-layout.tsx b/packages/js/product-editor/src/product-section-layout/product-section-layout.tsx similarity index 89% rename from packages/js/components/src/product-section-layout/product-section-layout.tsx rename to packages/js/product-editor/src/product-section-layout/product-section-layout.tsx index 5c80ab9e426..1a64941b85d 100644 --- a/packages/js/components/src/product-section-layout/product-section-layout.tsx +++ b/packages/js/product-editor/src/product-section-layout/product-section-layout.tsx @@ -2,11 +2,7 @@ * External dependencies */ import { Children, isValidElement, createElement } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { FormSection } from '../form-section'; +import { FormSection } from '@woocommerce/components'; type ProductSectionLayoutProps = { title: string; diff --git a/packages/js/components/src/product-section-layout/style.scss b/packages/js/product-editor/src/product-section-layout/style.scss similarity index 100% rename from packages/js/components/src/product-section-layout/style.scss rename to packages/js/product-editor/src/product-section-layout/style.scss diff --git a/packages/js/product-editor/src/style.scss b/packages/js/product-editor/src/style.scss new file mode 100644 index 00000000000..1e0e508f153 --- /dev/null +++ b/packages/js/product-editor/src/style.scss @@ -0,0 +1 @@ +@import 'product-section-layout/style.scss'; diff --git a/packages/js/product-editor/tsconfig-cjs.json b/packages/js/product-editor/tsconfig-cjs.json new file mode 100644 index 00000000000..035d2318baf --- /dev/null +++ b/packages/js/product-editor/tsconfig-cjs.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig-cjs", + "compilerOptions": { + "outDir": "build" + } +} diff --git a/packages/js/product-editor/tsconfig.json b/packages/js/product-editor/tsconfig.json new file mode 100644 index 00000000000..ea9f201d401 --- /dev/null +++ b/packages/js/product-editor/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig", + "compilerOptions": { + "rootDir": "src", + "outDir": "build-module", + "declaration": true, + "declarationMap": true, + "declarationDir": "./build-types" + } +} diff --git a/packages/js/product-editor/webpack.config.js b/packages/js/product-editor/webpack.config.js new file mode 100644 index 00000000000..46a04612713 --- /dev/null +++ b/packages/js/product-editor/webpack.config.js @@ -0,0 +1,18 @@ +/** + * Internal dependencies + */ +const { webpackConfig } = require( '@woocommerce/internal-style-build' ); + +module.exports = { + mode: process.env.NODE_ENV || 'development', + entry: { + 'build-style': __dirname + '/src/style.scss', + }, + output: { + path: __dirname, + }, + module: { + rules: webpackConfig.rules, + }, + plugins: webpackConfig.plugins, +}; diff --git a/plugins/woocommerce-admin/client/products/fills/attributes-section/attributes-section-fills.tsx b/plugins/woocommerce-admin/client/products/fills/attributes-section/attributes-section-fills.tsx index 270e316d061..38822e65297 100644 --- a/plugins/woocommerce-admin/client/products/fills/attributes-section/attributes-section-fills.tsx +++ b/plugins/woocommerce-admin/client/products/fills/attributes-section/attributes-section-fills.tsx @@ -5,9 +5,9 @@ import { __ } from '@wordpress/i18n'; import { __experimentalWooProductSectionItem as WooProductSectionItem, __experimentalWooProductFieldItem as WooProductFieldItem, - __experimentalProductSectionLayout as ProductSectionLayout, Link, } from '@woocommerce/components'; +import { __experimentalProductSectionLayout as ProductSectionLayout } from '@woocommerce/product-editor'; import { recordEvent } from '@woocommerce/tracks'; /** diff --git a/plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx b/plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx index bfcfd642117..9bb7df9a001 100644 --- a/plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx +++ b/plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx @@ -5,8 +5,8 @@ import { __ } from '@wordpress/i18n'; import { __experimentalWooProductSectionItem as WooProductSectionItem, __experimentalWooProductFieldItem as WooProductFieldItem, - __experimentalProductFieldSection as ProductFieldSection, } from '@woocommerce/components'; +import { __experimentalProductFieldSection as ProductFieldSection } from '@woocommerce/product-editor'; /** * Internal dependencies diff --git a/plugins/woocommerce-admin/client/products/fills/images-section/images-section-fills.tsx b/plugins/woocommerce-admin/client/products/fills/images-section/images-section-fills.tsx index 6a9b88df499..685f27b821e 100644 --- a/plugins/woocommerce-admin/client/products/fills/images-section/images-section-fills.tsx +++ b/plugins/woocommerce-admin/client/products/fills/images-section/images-section-fills.tsx @@ -5,9 +5,9 @@ import { __ } from '@wordpress/i18n'; import { __experimentalWooProductSectionItem as WooProductSectionItem, __experimentalWooProductFieldItem as WooProductFieldItem, - __experimentalProductFieldSection as ProductFieldSection, Link, } from '@woocommerce/components'; +import { __experimentalProductFieldSection as ProductFieldSection } from '@woocommerce/product-editor'; import { recordEvent } from '@woocommerce/tracks'; /** diff --git a/plugins/woocommerce-admin/client/products/fills/inventory-section/inventory-section-fills.tsx b/plugins/woocommerce-admin/client/products/fills/inventory-section/inventory-section-fills.tsx index efcce2629ee..1f85d91780e 100644 --- a/plugins/woocommerce-admin/client/products/fills/inventory-section/inventory-section-fills.tsx +++ b/plugins/woocommerce-admin/client/products/fills/inventory-section/inventory-section-fills.tsx @@ -5,11 +5,11 @@ import { __ } from '@wordpress/i18n'; import { __experimentalWooProductSectionItem as WooProductSectionItem, __experimentalWooProductFieldItem as WooProductFieldItem, - __experimentalProductSectionLayout as ProductSectionLayout, Link, useFormContext, CollapsibleContent, } from '@woocommerce/components'; +import { __experimentalProductSectionLayout as ProductSectionLayout } from '@woocommerce/product-editor'; import { Card, CardBody } from '@wordpress/components'; import { recordEvent } from '@woocommerce/tracks'; import { getAdminLink } from '@woocommerce/settings'; diff --git a/plugins/woocommerce-admin/client/products/fills/pricing-section/pricing-section-fills.tsx b/plugins/woocommerce-admin/client/products/fills/pricing-section/pricing-section-fills.tsx index 24d9a10d21a..6329d7e19f9 100644 --- a/plugins/woocommerce-admin/client/products/fills/pricing-section/pricing-section-fills.tsx +++ b/plugins/woocommerce-admin/client/products/fills/pricing-section/pricing-section-fills.tsx @@ -5,11 +5,11 @@ import { __ } from '@wordpress/i18n'; import { __experimentalWooProductSectionItem as WooProductSectionItem, __experimentalWooProductFieldItem as WooProductFieldItem, - __experimentalProductSectionLayout as ProductSectionLayout, Link, useFormContext, CollapsibleContent, } from '@woocommerce/components'; +import { __experimentalProductSectionLayout as ProductSectionLayout } from '@woocommerce/product-editor'; import { recordEvent } from '@woocommerce/tracks'; import { Product } from '@woocommerce/data'; import { useContext } from '@wordpress/element'; diff --git a/plugins/woocommerce-admin/client/products/fills/product-form-api-section-fills.tsx b/plugins/woocommerce-admin/client/products/fills/product-form-api-section-fills.tsx index 6df4347e24f..7c1e68acef5 100644 --- a/plugins/woocommerce-admin/client/products/fills/product-form-api-section-fills.tsx +++ b/plugins/woocommerce-admin/client/products/fills/product-form-api-section-fills.tsx @@ -1,10 +1,9 @@ /** * External dependencies */ -import { - __experimentalWooProductSectionItem as WooProductSectionItem, - __experimentalProductFieldSection as ProductFieldSection, -} from '@woocommerce/components'; +import { __ } from '@wordpress/i18n'; +import { __experimentalWooProductSectionItem as WooProductSectionItem } from '@woocommerce/components'; +import { __experimentalProductFieldSection as ProductFieldSection } from '@woocommerce/product-editor'; import { ProductFormSection } from '@woocommerce/data'; export const Sections: React.FC< { sections: ProductFormSection[] } > = ( { diff --git a/plugins/woocommerce-admin/client/products/fills/shipping-section/shipping-section-fills.tsx b/plugins/woocommerce-admin/client/products/fills/shipping-section/shipping-section-fills.tsx index 27e8916a810..262d15b4e98 100644 --- a/plugins/woocommerce-admin/client/products/fills/shipping-section/shipping-section-fills.tsx +++ b/plugins/woocommerce-admin/client/products/fills/shipping-section/shipping-section-fills.tsx @@ -5,8 +5,8 @@ import { __ } from '@wordpress/i18n'; import { __experimentalWooProductSectionItem as WooProductSectionItem, __experimentalWooProductFieldItem as WooProductFieldItem, - __experimentalProductSectionLayout as ProductSectionLayout, } from '@woocommerce/components'; +import { __experimentalProductSectionLayout as ProductSectionLayout } from '@woocommerce/product-editor'; import { PartialProduct, OPTIONS_STORE_NAME } from '@woocommerce/data'; import { useSelect } from '@wordpress/data'; import { useState } from '@wordpress/element'; diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 7671222bec5..49b527f4916 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -151,6 +151,7 @@ "@woocommerce/navigation": "workspace:*", "@woocommerce/notices": "workspace:*", "@woocommerce/number": "workspace:*", + "@woocommerce/product-editor": "workspace:*", "@woocommerce/onboarding": "workspace:*", "@woocommerce/tracks": "workspace:*", "@wordpress/babel-preset-default": "^6.5.1", diff --git a/plugins/woocommerce-admin/webpack.config.js b/plugins/woocommerce-admin/webpack.config.js index 00b70e43f47..258d7eaeae3 100644 --- a/plugins/woocommerce-admin/webpack.config.js +++ b/plugins/woocommerce-admin/webpack.config.js @@ -36,6 +36,7 @@ const wcAdminPackages = [ 'data', 'tracks', 'onboarding', + 'product-editor', ]; // wpAdminScripts are loaded on wp-admin pages outside the context of WooCommerce Admin // See ./client/wp-admin-scripts/README.md for more details diff --git a/plugins/woocommerce/changelog/add-36355_new_product_editor_package b/plugins/woocommerce/changelog/add-36355_new_product_editor_package new file mode 100644 index 00000000000..2287510a5c9 --- /dev/null +++ b/plugins/woocommerce/changelog/add-36355_new_product_editor_package @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add @woocommerce/product-editor dependency and change dependency of ProductSectionLayout component. diff --git a/plugins/woocommerce/src/Internal/Admin/WCAdminAssets.php b/plugins/woocommerce/src/Internal/Admin/WCAdminAssets.php index bf8d399f47d..c2887a48356 100644 --- a/plugins/woocommerce/src/Internal/Admin/WCAdminAssets.php +++ b/plugins/woocommerce/src/Internal/Admin/WCAdminAssets.php @@ -263,6 +263,7 @@ class WCAdminAssets { 'wc-store-data', 'wc-currency', 'wc-navigation', + 'wc-product-editor', ); $scripts_map = array( @@ -311,6 +312,14 @@ class WCAdminAssets { ); wp_style_add_data( 'wc-components', 'rtl', 'replace' ); + wp_register_style( + 'wc-product-editor', + self::get_url( 'product-editor/style', 'css' ), + array(), + $css_file_version + ); + wp_style_add_data( 'wc-product-editor', 'rtl', 'replace' ); + wp_register_style( 'wc-customer-effort-score', self::get_url( 'customer-effort-score/style', 'css' ), @@ -339,7 +348,7 @@ class WCAdminAssets { wp_register_style( WC_ADMIN_APP, self::get_url( 'app/style', 'css' ), - array( 'wc-components', 'wc-customer-effort-score', 'wp-components', 'wc-experimental' ), + array( 'wc-components', 'wc-customer-effort-score', 'wc-product-editor', 'wp-components', 'wc-experimental' ), $css_file_version ); wp_style_add_data( WC_ADMIN_APP, 'rtl', 'replace' ); @@ -370,6 +379,7 @@ class WCAdminAssets { 'wc-date', 'wc-components', 'wc-tracks', + 'wc-product-editor', ]; foreach ( $handles_for_injection as $handle ) { $script = wp_scripts()->query( $handle, 'registered' ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 75891be5b4e..3f668e27b27 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1289,6 +1289,45 @@ importers: webpack: 5.70.0_webpack-cli@3.3.12 webpack-cli: 3.3.12_webpack@5.70.0 + packages/js/product-editor: + specifiers: + '@types/wordpress__components': ^19.10.1 + '@woocommerce/components': workspace:* + '@woocommerce/eslint-plugin': workspace:* + '@woocommerce/internal-style-build': workspace:* + '@wordpress/browserslist-config': ^4.1.1 + '@wordpress/components': ^19.5.0 + '@wordpress/element': ^4.1.1 + css-loader: ^3.6.0 + eslint: ^8.12.0 + jest: ^27.5.1 + jest-cli: ^27.5.1 + postcss-loader: ^3.0.0 + sass-loader: ^10.2.1 + ts-jest: ^27.1.3 + typescript: ^4.8.3 + webpack: ^5.70.0 + webpack-cli: ^3.3.12 + dependencies: + '@woocommerce/components': link:../components + '@wordpress/components': 19.12.0_rysvg2ttzfworbkpz2ftlx73d4 + '@wordpress/element': 4.20.0 + devDependencies: + '@types/wordpress__components': 19.10.1_prpqlkd37azqwypxturxi7uyci + '@woocommerce/eslint-plugin': link:../eslint-plugin + '@woocommerce/internal-style-build': link:../internal-style-build + '@wordpress/browserslist-config': 4.1.3 + css-loader: 3.6.0_webpack@5.70.0 + eslint: 8.28.0 + jest: 27.5.1 + jest-cli: 27.5.1 + postcss-loader: 3.0.0 + sass-loader: 10.2.1_webpack@5.70.0 + ts-jest: 27.1.3_77oryishcckaigojnzbhxsiona + typescript: 4.8.4 + webpack: 5.70.0_webpack-cli@3.3.12 + webpack-cli: 3.3.12_webpack@5.70.0 + packages/js/tracks: specifiers: '@babel/core': ^7.17.5 @@ -1474,6 +1513,7 @@ importers: '@woocommerce/notices': workspace:* '@woocommerce/number': workspace:* '@woocommerce/onboarding': workspace:* + '@woocommerce/product-editor': workspace:* '@woocommerce/tracks': workspace:* '@wordpress/a11y': ^3.5.0 '@wordpress/api-fetch': ^6.0.1 @@ -1689,6 +1729,7 @@ importers: '@woocommerce/notices': link:../../packages/js/notices '@woocommerce/number': link:../../packages/js/number '@woocommerce/onboarding': link:../../packages/js/onboarding + '@woocommerce/product-editor': link:../../packages/js/product-editor '@woocommerce/tracks': link:../../packages/js/tracks '@wordpress/babel-preset-default': 6.6.1 '@wordpress/block-editor': 9.8.0_vcke6catv4iqpjdw24uwvlzyyi @@ -4094,7 +4135,7 @@ packages: '@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.19.0 + '@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 @@ -4109,7 +4150,7 @@ packages: '@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.19.0 + '@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 @@ -7495,7 +7536,7 @@ packages: resolution: {integrity: sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A==} dependencies: '@emotion/memoize': 0.7.5 - '@emotion/sheet': 1.1.0 + '@emotion/sheet': 1.2.1 '@emotion/utils': 1.2.0 '@emotion/weak-memoize': 0.2.5 stylis: 4.0.13 @@ -7533,7 +7574,7 @@ packages: '@emotion/babel-plugin': 11.10.5_@babel+core@7.17.8 '@emotion/cache': 11.10.5 '@emotion/serialize': 1.1.1 - '@emotion/sheet': 1.1.0 + '@emotion/sheet': 1.2.1 '@emotion/utils': 1.2.0 /@emotion/hash/0.8.0: @@ -7664,9 +7705,6 @@ packages: /@emotion/sheet/0.9.4: resolution: {integrity: sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==} - /@emotion/sheet/1.1.0: - resolution: {integrity: sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==} - /@emotion/sheet/1.2.1: resolution: {integrity: sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==} @@ -7812,8 +7850,8 @@ packages: dependencies: ajv: 6.12.6 debug: 4.3.4 - espree: 9.4.0 - globals: 13.17.0 + espree: 9.4.1 + globals: 13.18.0 ignore: 4.0.6 import-fresh: 3.3.0 js-yaml: 4.1.0 @@ -7829,8 +7867,8 @@ packages: dependencies: ajv: 6.12.6 debug: 4.3.4 - espree: 9.4.0 - globals: 13.17.0 + espree: 9.4.1 + globals: 13.18.0 ignore: 5.2.0 import-fresh: 3.3.0 js-yaml: 4.1.0 @@ -7896,6 +7934,20 @@ packages: - '@types/react' dev: false + /@floating-ui/react-dom/0.6.3_prpqlkd37azqwypxturxi7uyci: + resolution: {integrity: sha512-hC+pS5D6AgS2wWjbmSQ6UR6Kpy+drvWGJIri6e1EDGADTPsCaa4KzCgmCczHrQeInx9tqs81EyDmbKJYY2swKg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@floating-ui/dom': 0.4.5 + react: 17.0.2 + react-dom: 16.14.0_react@17.0.2 + use-isomorphic-layout-effect: 1.1.1_react@17.0.2 + transitivePeerDependencies: + - '@types/react' + dev: false + /@floating-ui/react-dom/1.0.0_sfoxds7t5ydpegc3knd667wn6m: resolution: {integrity: sha512-uiOalFKPG937UCLm42RxjESTWUVpbbatvlphQAU6bsv+ence6IoVG8JOUZcy8eW81NkU+Idiwvx10WFLmR4MIg==} peerDependencies: @@ -8206,7 +8258,7 @@ packages: '@jest/test-result': 27.5.1 '@jest/transform': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 17.0.21 + '@types/node': 18.11.18 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.8.1 @@ -8792,7 +8844,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.3 '@types/istanbul-reports': 3.0.1 - '@types/node': 17.0.21 + '@types/node': 18.11.18 '@types/yargs': 16.0.4 chalk: 4.1.2 @@ -15473,6 +15525,60 @@ packages: - react-with-direction dev: false + /@wordpress/components/19.12.0_rysvg2ttzfworbkpz2ftlx73d4: + resolution: {integrity: sha512-Ac1+aIMM7NDgN3G7i5kcaETSvZfeqB4U6PubApPmM6FdBF5VfkYUZeqNcC7cuJdveyokRrqHg11/l+DcJGA7/g==} + engines: {node: '>=12'} + peerDependencies: + react: ^17.0.0 + react-dom: ^17.0.0 + dependencies: + '@babel/runtime': 7.19.0 + '@emotion/cache': 11.10.5 + '@emotion/css': 11.7.1_@babel+core@7.17.8 + '@emotion/react': 11.10.5_mcptgafjogap2nfvnfqvfwh6uu + '@emotion/serialize': 1.1.1 + '@emotion/styled': 11.8.1_c2qm47vaialpqni522adyu6za4 + '@emotion/utils': 1.0.0 + '@floating-ui/react-dom': 0.6.3_prpqlkd37azqwypxturxi7uyci + '@use-gesture/react': 10.2.10_react@17.0.2 + '@wordpress/a11y': 3.19.0 + '@wordpress/compose': 5.17.0_react@17.0.2 + '@wordpress/date': 4.19.0 + '@wordpress/deprecated': 3.19.0 + '@wordpress/dom': 3.19.0 + '@wordpress/element': 4.20.0 + '@wordpress/escape-html': 2.22.0 + '@wordpress/hooks': 3.19.0 + '@wordpress/i18n': 4.19.0 + '@wordpress/icons': 9.10.0 + '@wordpress/is-shallow-equal': 4.19.0 + '@wordpress/keycodes': 3.19.0 + '@wordpress/primitives': 3.17.0 + '@wordpress/rich-text': 5.17.0_react@17.0.2 + '@wordpress/warning': 2.19.0 + classnames: 2.3.1 + colord: 2.9.2 + dom-scroll-into-view: 1.2.1 + downshift: 6.1.12_react@17.0.2 + framer-motion: 6.2.8_prpqlkd37azqwypxturxi7uyci + gradient-parser: 0.1.5 + highlight-words-core: 1.2.2 + lodash: 4.17.21 + memize: 1.1.0 + moment: 2.29.1 + re-resizable: 6.9.5_prpqlkd37azqwypxturxi7uyci + react: 17.0.2 + react-colorful: 5.5.1_prpqlkd37azqwypxturxi7uyci + react-dates: 21.8.0_itvjtpkzdblwka3tnmrs5t5e6y + react-dom: 16.14.0_react@17.0.2 + reakit: 1.3.11_prpqlkd37azqwypxturxi7uyci + uuid: 8.3.2 + transitivePeerDependencies: + - '@babel/core' + - '@types/react' + - react-with-direction + dev: false + /@wordpress/components/19.12.0_tufdcic6wklrwyy3rhbsbktylu: resolution: {integrity: sha512-Ac1+aIMM7NDgN3G7i5kcaETSvZfeqB4U6PubApPmM6FdBF5VfkYUZeqNcC7cuJdveyokRrqHg11/l+DcJGA7/g==} engines: {node: '>=12'} @@ -16302,7 +16408,7 @@ packages: '@babel/runtime': 7.19.0 '@types/react': 16.14.31 '@types/react-dom': 16.9.16 - '@wordpress/escape-html': 2.19.0 + '@wordpress/escape-html': 2.22.0 lodash: 4.17.21 react: 17.0.2 react-dom: 17.0.2_react@17.0.2 @@ -16366,12 +16472,6 @@ packages: '@babel/runtime': 7.19.0 dev: false - /@wordpress/escape-html/2.19.0: - resolution: {integrity: sha512-pBMuDDaV15SGXNu4cSu1pYiavkcx1rCBOuzGTSJ/WYLAC+K6PK+1lacPsPajVqnm2LLeKkNG4b9yn3PSZlbCww==} - engines: {node: '>=12'} - dependencies: - '@babel/runtime': 7.19.0 - /@wordpress/escape-html/2.22.0: resolution: {integrity: sha512-GUo6VLugIZxen1rdYuotvz6Vqa+5fNtVelNjXLwDqRu0iY2RXeoTux9V5bZWXPnGb54ryqfYmR4gH6F8xZhWzQ==} engines: {node: '>=12'} @@ -17226,7 +17326,7 @@ packages: '@wordpress/compose': 5.17.0_react@17.0.2 '@wordpress/data': 6.15.0_react@17.0.2 '@wordpress/element': 4.20.0 - '@wordpress/escape-html': 2.19.0 + '@wordpress/escape-html': 2.22.0 '@wordpress/i18n': 4.19.0 '@wordpress/keycodes': 3.19.0 lodash: 4.17.21 @@ -17745,13 +17845,6 @@ packages: acorn: 8.7.0 dev: true - /acorn-jsx/5.3.2_acorn@8.8.0: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - acorn: 8.8.0 - /acorn-jsx/5.3.2_acorn@8.8.1: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -17792,11 +17885,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - /acorn/8.8.0: - resolution: {integrity: sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==} - engines: {node: '>=0.4.0'} - hasBin: true - /acorn/8.8.1: resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} engines: {node: '>=0.4.0'} @@ -24083,8 +24171,8 @@ packages: resolution: {integrity: sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - acorn: 8.8.0 - acorn-jsx: 5.3.2_acorn@8.8.0 + acorn: 8.8.1 + acorn-jsx: 5.3.2_acorn@8.8.1 eslint-visitor-keys: 3.3.0 /espree/9.4.1: @@ -25944,7 +26032,7 @@ packages: dev: true /good-listener/1.2.2: - resolution: {integrity: sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=} + resolution: {integrity: sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==} dependencies: delegate: 3.2.0 @@ -26965,7 +27053,7 @@ packages: dev: true /import-cwd/2.1.0: - resolution: {integrity: sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=} + resolution: {integrity: sha512-Ew5AZzJQFqrOV5BTW3EIoHAnoie1LojZLXKcCQ/yTRyVZosBhK1x1ViYjHGf5pAFOq8ZyChZp6m/fSN7pJyZtg==} engines: {node: '>=4'} dependencies: import-from: 2.1.0 @@ -26985,7 +27073,7 @@ packages: resolve-from: 4.0.0 /import-from/2.1.0: - resolution: {integrity: sha1-M1238qev/VOqpHHUuAId7ja387E=} + resolution: {integrity: sha512-0vdnLL2wSGnhlRmzHJAg5JHjt1l2vYhzJ7tNLGbeVg0fse56tpGaH0uzH+r9Slej+BSXXEHvBKDEnVSLLE9/+w==} engines: {node: '>=4'} dependencies: resolve-from: 3.0.0 @@ -27514,7 +27602,7 @@ packages: dev: true /is-path-inside/1.0.1: - resolution: {integrity: sha1-jvW33lBDej/cprToZe96pVy0gDY=} + resolution: {integrity: sha512-qhsCR/Esx4U4hg/9I19OVUAJkGWtjRYHMRgUMZE2TDdj+Ag+kttZanLupfddNyglzz50cUlmWzUaI37GDfNx/g==} engines: {node: '>=0.10.0'} dependencies: path-is-inside: 1.0.2 @@ -31187,11 +31275,11 @@ packages: dev: true /memory-fs/0.2.0: - resolution: {integrity: sha1-8rslNovBIeORwlIN6Slpyu4KApA=} + resolution: {integrity: sha512-+y4mDxU4rvXXu5UDSGCGNiesFmwCHuefGMoPCO1WYucNYj7DsLqrFaa2fXVI0H+NNiPTwwzKwspn9yTZqUGqng==} dev: true /memory-fs/0.4.1: - resolution: {integrity: sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=} + resolution: {integrity: sha512-cda4JKCxReDXFXRqOHPQscuIYg1PvxbE2S2GP45rnwfEK+vZaXC8C1OFvdHIbgw0DLzowXGVoxLaAmlgRy14GQ==} dependencies: errno: 0.1.8 readable-stream: 2.3.7 @@ -34665,7 +34753,7 @@ packages: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} /prr/1.0.1: - resolution: {integrity: sha1-0/wRS6BplaRexok/SEzrHXj19HY=} + resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} /pseudomap/1.0.2: resolution: {integrity: sha1-8FKijacOYYkX7wqKw0wa5aaChrM=} @@ -35129,6 +35217,36 @@ packages: react-with-styles-interface-css: 6.0.0_u5gvwsivijvqc4cln26hqg7igq dev: false + /react-dates/21.8.0_itvjtpkzdblwka3tnmrs5t5e6y: + resolution: {integrity: sha512-PPriGqi30CtzZmoHiGdhlA++YPYPYGCZrhydYmXXQ6RAvAsaONcPtYgXRTLozIOrsQ5mSo40+DiA5eOFHnZ6xw==} + peerDependencies: + '@babel/runtime': ^7.0.0 + moment: ^2.18.1 + react: ^0.14 || ^15.5.4 || ^16.1.1 + react-dom: ^0.14 || ^15.5.4 || ^16.1.1 + react-with-direction: ^1.3.1 + dependencies: + '@babel/runtime': 7.19.0 + airbnb-prop-types: 2.16.0_react@17.0.2 + consolidated-events: 2.0.2 + enzyme-shallow-equal: 1.0.4 + is-touch-device: 1.0.1 + lodash: 4.17.21 + moment: 2.29.1 + object.assign: 4.1.4 + object.values: 1.1.5 + prop-types: 15.8.1 + raf: 3.4.1 + react: 17.0.2 + react-dom: 16.14.0_react@17.0.2 + react-moment-proptypes: 1.8.1_moment@2.29.1 + react-outside-click-handler: 1.3.0_prpqlkd37azqwypxturxi7uyci + react-portal: 4.2.1_prpqlkd37azqwypxturxi7uyci + react-with-direction: 1.4.0_prpqlkd37azqwypxturxi7uyci + react-with-styles: 4.2.0_tzgwoaxjvs23ve2qhnwxwqxt3e + react-with-styles-interface-css: 6.0.0_sjrqpgd5uboanyy2xkv2xcu6vm + dev: false + /react-dates/21.8.0_ruqm5uqcpfavftrrblmrkdsf44: resolution: {integrity: sha512-PPriGqi30CtzZmoHiGdhlA++YPYPYGCZrhydYmXXQ6RAvAsaONcPtYgXRTLozIOrsQ5mSo40+DiA5eOFHnZ6xw==} peerDependencies: @@ -35145,7 +35263,7 @@ packages: is-touch-device: 1.0.1 lodash: 4.17.21 moment: 2.29.1 - object.assign: 4.1.2 + object.assign: 4.1.4 object.values: 1.1.5 prop-types: 15.8.1 raf: 3.4.1 @@ -35755,6 +35873,24 @@ packages: react-dom: 17.0.2_react@17.0.2 dev: false + /react-with-direction/1.4.0_prpqlkd37azqwypxturxi7uyci: + resolution: {integrity: sha512-ybHNPiAmaJpoWwugwqry9Hd1Irl2hnNXlo/2SXQBwbLn/jGMauMS2y9jw+ydyX5V9ICryCqObNSthNt5R94xpg==} + peerDependencies: + react: ^0.14 || ^15 || ^16 + react-dom: ^0.14 || ^15 || ^16 + dependencies: + airbnb-prop-types: 2.16.0_react@17.0.2 + brcast: 2.0.2 + deepmerge: 1.5.2 + direction: 1.0.4 + hoist-non-react-statics: 3.3.2 + object.assign: 4.1.4 + object.values: 1.1.5 + prop-types: 15.8.1 + react: 17.0.2 + react-dom: 16.14.0_react@17.0.2 + dev: false + /react-with-direction/1.4.0_sfoxds7t5ydpegc3knd667wn6m: resolution: {integrity: sha512-ybHNPiAmaJpoWwugwqry9Hd1Irl2hnNXlo/2SXQBwbLn/jGMauMS2y9jw+ydyX5V9ICryCqObNSthNt5R94xpg==} peerDependencies: @@ -35833,7 +35969,7 @@ packages: object.assign: 4.1.4 prop-types: 15.8.1 react: 17.0.2 - react-with-direction: 1.4.0_sfoxds7t5ydpegc3knd667wn6m + react-with-direction: 1.4.0_prpqlkd37azqwypxturxi7uyci dev: false /react-with-styles/4.2.0_vnnik6nzwz6bu5mwl5zhriyd4i: @@ -37039,7 +37175,7 @@ packages: dev: false /select/1.1.2: - resolution: {integrity: sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=} + resolution: {integrity: sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==} /semver-diff/2.1.0: resolution: {integrity: sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=} @@ -40145,7 +40281,6 @@ packages: optional: true dependencies: react: 17.0.2 - dev: true /use-latest/1.2.0_pxzommwrsowkd4kgag6q3sluym: resolution: {integrity: sha512-d2TEuG6nSLKQLAfW3By8mKr8HurOlTkul0sOpxbClIv4SQ4iOd7BYr7VIzdbktUCnv7dua/60xzd8igMU6jmyw==} From d59e7e57210abaf93b30cda8d3c0788f1315378d Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Tue, 31 Jan 2023 23:39:02 +0800 Subject: [PATCH 124/228] Edit and arrange imports in MarketingOverviewMultichannel. --- .../MarketingOverviewMultichannel.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx index 838a45ecbf0..2e899bf5774 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx @@ -6,19 +6,19 @@ import { useUser } from '@woocommerce/data'; /** * Internal dependencies */ +import '~/marketing/data'; +import '~/marketing/data-multichannel'; +import { CenteredSpinner } from '~/marketing/components'; +import { + useRegisteredChannels, + useRecommendedChannels, +} from '~/marketing/hooks'; import { getAdminSetting } from '~/utils/admin-settings'; import { Channels } from './Channels'; import { InstalledExtensions } from './InstalledExtensions'; import { DiscoverTools } from './DiscoverTools'; import { LearnMarketing } from './LearnMarketing'; -import '~/marketing/data'; -import '~/marketing/data-multichannel'; -import { - useRegisteredChannels, - useRecommendedChannels, -} from '~/marketing/hooks'; import './MarketingOverviewMultichannel.scss'; -import { CenteredSpinner } from '../components'; export const MarketingOverviewMultichannel: React.FC = () => { const { From add0c4a5be368544c7e815b6a426747924012a90 Mon Sep 17 00:00:00 2001 From: nigeljamesstevenson Date: Tue, 31 Jan 2023 16:29:15 +0000 Subject: [PATCH 125/228] update after code review --- .github/workflows/smoke-test-daily.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/smoke-test-daily.yml b/.github/workflows/smoke-test-daily.yml index 8643b6d395b..bf4e89ea759 100644 --- a/.github/workflows/smoke-test-daily.yml +++ b/.github/workflows/smoke-test-daily.yml @@ -22,7 +22,6 @@ jobs: runs-on: ubuntu-20.04 permissions: contents: read - if: success() || failure() env: ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-results ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-report @@ -257,7 +256,7 @@ jobs: runs-on: ubuntu-20.04 permissions: contents: read - needs: [test-plugins, k6-tests] + needs: [e2e-tests, test-plugins, k6-tests] steps: - name: Create dirs run: | @@ -313,7 +312,7 @@ jobs: ( success() || failure() ) && ! github.event.pull_request.head.repo.fork runs-on: ubuntu-20.04 - needs: [test-plugins, k6-tests] + needs: [e2e-tests, test-plugins, k6-tests] env: GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }} RUN_ID: ${{ github.run_id }} From 177fc59deac5f5277ee4c8469cc5424bd2320912 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Wed, 1 Feb 2023 00:29:38 +0800 Subject: [PATCH 126/228] Simplify rendering code in MarketingOverviewMultichannel. --- .../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 2e899bf5774..54c85682800 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx @@ -45,8 +45,7 @@ export const MarketingOverviewMultichannel: React.FC = () => {
{ dataRegistered && dataRecommended && - ( dataRegistered.length >= 1 || - dataRecommended.length >= 1 ) && ( + ( dataRegistered.length || dataRecommended.length ) && ( Date: Tue, 31 Jan 2023 17:39:10 +0100 Subject: [PATCH 127/228] Disable product info in Status Widget when stock management is disabled There's no need to show the two products cells in Status Widget table when stock management is disabled, otherwise you'll get 2 links pointing nowhere (specific report pages are disabled and you'll get an "unauthorized" error) --- .../woocommerce/includes/admin/class-wc-admin-dashboard.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-dashboard.php b/plugins/woocommerce/includes/admin/class-wc-admin-dashboard.php index d87525f252e..bcb21a4f010 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-dashboard.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-dashboard.php @@ -183,7 +183,9 @@ if ( ! class_exists( 'WC_Admin_Dashboard', false ) ) : } $this->status_widget_order_rows(); - $this->status_widget_stock_rows( $is_wc_admin_disabled ); + if ( get_option( 'woocommerce_manage_stock' ) === 'yes' ) { + $this->status_widget_stock_rows( $is_wc_admin_disabled ); + } do_action( 'woocommerce_after_dashboard_status_widget', $reports ); echo ''; From f050bb79b533970b52dd885763d94ce2e586fb21 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Wed, 1 Feb 2023 00:52:35 +0800 Subject: [PATCH 128/228] Rename Channel to RegisteredChannel. This is for better clarity and consistency. --- .../marketing/data-multichannel/action-types.ts | 6 ++++-- .../marketing/data-multichannel/actions.ts | 16 +++++++++------- .../marketing/data-multichannel/reducer.ts | 10 +++++----- .../marketing/data-multichannel/resolvers.ts | 14 +++++++------- .../marketing/data-multichannel/selectors.ts | 4 ++-- .../client/marketing/data-multichannel/types.ts | 6 +++--- .../marketing/hooks/useRegisteredChannels.ts | 13 +++++++------ 7 files changed, 37 insertions(+), 32 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 c53be7bbd49..ab0e40f15ae 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts @@ -1,6 +1,8 @@ export const TYPES = { - RECEIVE_CHANNELS_SUCCESS: 'RECEIVE_CHANNELS_SUCCESS' as const, - RECEIVE_CHANNELS_ERROR: 'RECEIVE_CHANNELS_ERROR' as const, + RECEIVE_REGISTERED_CHANNELS_SUCCESS: + 'RECEIVE_REGISTERED_CHANNELS_SUCCESS' as const, + RECEIVE_REGISTERED_CHANNELS_ERROR: + 'RECEIVE_REGISTERED_CHANNELS_ERROR' as const, RECEIVE_RECOMMENDED_CHANNELS_SUCCESS: 'RECEIVE_RECOMMENDED_CHANNELS_SUCCESS' as const, RECEIVE_RECOMMENDED_CHANNELS_ERROR: diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts index b07417e420e..685aa867c02 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts @@ -2,18 +2,20 @@ * Internal dependencies */ import { TYPES } from './action-types'; -import { ApiFetchError, Channel, RecommendedChannel } from './types'; +import { ApiFetchError, RegisteredChannel, RecommendedChannel } from './types'; -export const receiveChannelsSuccess = ( channels: Array< Channel > ) => { +export const receiveRegisteredChannelsSuccess = ( + channels: Array< RegisteredChannel > +) => { return { - type: TYPES.RECEIVE_CHANNELS_SUCCESS, + type: TYPES.RECEIVE_REGISTERED_CHANNELS_SUCCESS, payload: channels, }; }; -export const receiveChannelsError = ( error: ApiFetchError ) => { +export const receiveRegisteredChannelsError = ( error: ApiFetchError ) => { return { - type: TYPES.RECEIVE_CHANNELS_ERROR, + type: TYPES.RECEIVE_REGISTERED_CHANNELS_ERROR, payload: error, error: true, }; @@ -37,8 +39,8 @@ export const receiveRecommendedChannelsError = ( error: ApiFetchError ) => { }; export type Action = ReturnType< - | typeof receiveChannelsSuccess - | typeof receiveChannelsError + | typeof receiveRegisteredChannelsSuccess + | typeof receiveRegisteredChannelsError | 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 d9e95181bf6..82749ee49f6 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts @@ -12,7 +12,7 @@ import { Action } from './actions'; import { TYPES } from './action-types'; const initialState = { - channels: { + registeredChannels: { data: undefined, error: undefined, }, @@ -27,17 +27,17 @@ export const reducer: Reducer< State, Action > = ( action ) => { switch ( action.type ) { - case TYPES.RECEIVE_CHANNELS_SUCCESS: + case TYPES.RECEIVE_REGISTERED_CHANNELS_SUCCESS: return { ...state, - channels: { + registeredChannels: { data: action.payload, }, }; - case TYPES.RECEIVE_CHANNELS_ERROR: + case TYPES.RECEIVE_REGISTERED_CHANNELS_ERROR: return { ...state, - channels: { + registeredChannels: { error: action.payload, }, }; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts index c0feb6b2985..771a3b084c3 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts @@ -7,25 +7,25 @@ import { apiFetch } from '@wordpress/data-controls'; * Internal dependencies */ import { - receiveChannelsSuccess, - receiveChannelsError, + receiveRegisteredChannelsSuccess, + receiveRegisteredChannelsError, receiveRecommendedChannelsSuccess, receiveRecommendedChannelsError, } from './actions'; -import { Channel, RecommendedChannel } from './types'; +import { RegisteredChannel, RecommendedChannel } from './types'; import { API_NAMESPACE } from './constants'; import { isApiFetchError } from './guards'; -export function* getChannels() { +export function* getRegisteredChannels() { try { - const data: Channel[] = yield apiFetch( { + const data: RegisteredChannel[] = yield apiFetch( { path: `${ API_NAMESPACE }/channels`, } ); - yield receiveChannelsSuccess( data ); + yield receiveRegisteredChannelsSuccess( data ); } catch ( error ) { if ( isApiFetchError( error ) ) { - yield receiveChannelsError( error ); + yield receiveRegisteredChannelsError( 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 c37bb8599ad..48df4625c01 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts @@ -3,8 +3,8 @@ */ import { State } from './types'; -export const getChannels = ( state: State ) => { - return state.channels; +export const getRegisteredChannels = ( state: State ) => { + return state.registeredChannels; }; export const getRecommendedChannels = ( state: State ) => { diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts index 518cdb03df7..f9e4dc44ee7 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts @@ -6,7 +6,7 @@ export type ApiFetchError = { message: string; }; -export type Channel = { +export type RegisteredChannel = { slug: string; is_setup_completed: boolean; settings_url: string; @@ -18,7 +18,7 @@ export type Channel = { }; export type ChannelsState = { - data?: Array< Channel >; + data?: Array< RegisteredChannel >; error?: ApiFetchError; }; @@ -51,6 +51,6 @@ export type RecommendedChannelsState = { }; export type State = { - channels: ChannelsState; + registeredChannels: ChannelsState; recommendedChannels: RecommendedChannelsState; }; diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts index 97337bc8d3d..1b4341d0694 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts @@ -12,7 +12,7 @@ import { RegisteredChannel, SyncStatusType } from '~/marketing/types'; import { STORE_KEY } from '~/marketing/data-multichannel/constants'; import { ApiFetchError, - Channel, + RegisteredChannel as APIRegisteredChannel, ChannelsState, } from '~/marketing/data-multichannel/types'; @@ -34,7 +34,7 @@ const statusMap: Record< string, SyncStatusType > = { synced: 'synced', }; -const convert = ( data: Channel ): RegisteredChannel => { +const convert = ( data: APIRegisteredChannel ): RegisteredChannel => { const issueType = data.errors_count >= 1 ? 'error' : 'none'; const issueText = data.errors_count >= 1 @@ -63,15 +63,16 @@ export const useRegisteredChannels = (): UseRegisteredChannels => { const { invalidateResolution } = useDispatch( STORE_KEY ); const refetch = useCallback( () => { - invalidateResolution( 'getChannels' ); + invalidateResolution( 'getRegisteredChannels' ); }, [ invalidateResolution ] ); return useSelect( ( select ) => { - const { hasFinishedResolution, getChannels } = select( STORE_KEY ); - const channels = getChannels< ChannelsState >(); + const { hasFinishedResolution, getRegisteredChannels } = + select( STORE_KEY ); + const channels = getRegisteredChannels< ChannelsState >(); return { - loading: ! hasFinishedResolution( 'getChannels' ), + loading: ! hasFinishedResolution( 'getRegisteredChannels' ), data: channels.data?.map( convert ), error: channels.error, refetch, From 4c427ef6daf3893f28e84e4c646ca8776b7f6eaf Mon Sep 17 00:00:00 2001 From: barryhughes <3594411+barryhughes@users.noreply.github.com> Date: Tue, 31 Jan 2023 09:56:43 -0800 Subject: [PATCH 129/228] Changelog. --- plugins/woocommerce/changelog/fix-dashboard-sales-widget | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-dashboard-sales-widget diff --git a/plugins/woocommerce/changelog/fix-dashboard-sales-widget b/plugins/woocommerce/changelog/fix-dashboard-sales-widget new file mode 100644 index 00000000000..15869dd89e5 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-dashboard-sales-widget @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Do not display low/out-of-stock information in the dashboard status widget when stock management is disabled. From 46adb0dc25e329ad3e1f7fb5d108c2f1c0131cd6 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Wed, 1 Feb 2023 02:58:49 +0800 Subject: [PATCH 130/228] Rename Channels to RegisteredChannels. --- .../client/marketing/data-multichannel/types.ts | 4 ++-- .../client/marketing/hooks/useRegisteredChannels.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts index f9e4dc44ee7..31cee8017c0 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts @@ -17,7 +17,7 @@ export type RegisteredChannel = { icon: string; }; -export type ChannelsState = { +export type RegisteredChannelsState = { data?: Array< RegisteredChannel >; error?: ApiFetchError; }; @@ -51,6 +51,6 @@ export type RecommendedChannelsState = { }; export type State = { - registeredChannels: ChannelsState; + registeredChannels: RegisteredChannelsState; recommendedChannels: RecommendedChannelsState; }; diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts index 1b4341d0694..bafbd5b6f2e 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts @@ -13,7 +13,7 @@ import { STORE_KEY } from '~/marketing/data-multichannel/constants'; import { ApiFetchError, RegisteredChannel as APIRegisteredChannel, - ChannelsState, + RegisteredChannelsState, } from '~/marketing/data-multichannel/types'; type UseRegisteredChannels = { @@ -69,12 +69,12 @@ export const useRegisteredChannels = (): UseRegisteredChannels => { return useSelect( ( select ) => { const { hasFinishedResolution, getRegisteredChannels } = select( STORE_KEY ); - const channels = getRegisteredChannels< ChannelsState >(); + const state = getRegisteredChannels< RegisteredChannelsState >(); return { loading: ! hasFinishedResolution( 'getRegisteredChannels' ), - data: channels.data?.map( convert ), - error: channels.error, + data: state.data?.map( convert ), + error: state.error, refetch, }; } ); From eeebcb9a6c4219b1735b796b8570c35638a4cf15 Mon Sep 17 00:00:00 2001 From: IanDelMar <42134098+IanDelMar@users.noreply.github.com> Date: Tue, 31 Jan 2023 22:02:42 +0100 Subject: [PATCH 131/228] Properly deprecate woocommerce_my_account_my_orders_columns filter (#36356) * Use WC_Deprecated_Filter_Hooks to deprecate filter * Add changelog file * fix since tag in comment * Revert docblock changes --- plugins/woocommerce/changelog/fix-filter-deprecation | 4 ++++ .../includes/class-wc-deprecated-filter-hooks.php | 2 ++ plugins/woocommerce/includes/wc-account-functions.php | 11 +++++++---- 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-filter-deprecation diff --git a/plugins/woocommerce/changelog/fix-filter-deprecation b/plugins/woocommerce/changelog/fix-filter-deprecation new file mode 100644 index 00000000000..dc383b325a3 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-filter-deprecation @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Add missing deprecation notice for filter hook woocommerce_my_account_my_orders_columns. diff --git a/plugins/woocommerce/includes/class-wc-deprecated-filter-hooks.php b/plugins/woocommerce/includes/class-wc-deprecated-filter-hooks.php index f65197945e2..1970192cd71 100644 --- a/plugins/woocommerce/includes/class-wc-deprecated-filter-hooks.php +++ b/plugins/woocommerce/includes/class-wc-deprecated-filter-hooks.php @@ -21,6 +21,7 @@ class WC_Deprecated_Filter_Hooks extends WC_Deprecated_Hooks { * @var array */ protected $deprecated_hooks = array( + 'woocommerce_account_orders_columns' => 'woocommerce_my_account_my_orders_columns', 'woocommerce_structured_data_order' => 'woocommerce_email_order_schema_markup', 'woocommerce_add_to_cart_fragments' => 'add_to_cart_fragments', 'woocommerce_add_to_cart_redirect' => 'add_to_cart_redirect', @@ -67,6 +68,7 @@ class WC_Deprecated_Filter_Hooks extends WC_Deprecated_Hooks { * @var array */ protected $deprecated_version = array( + 'woocommerce_my_account_my_orders_columns' => '2.6.0', 'woocommerce_email_order_schema_markup' => '3.0.0', 'add_to_cart_fragments' => '3.0.0', 'add_to_cart_redirect' => '3.0.0', diff --git a/plugins/woocommerce/includes/wc-account-functions.php b/plugins/woocommerce/includes/wc-account-functions.php index dfb779310b2..531176e7279 100644 --- a/plugins/woocommerce/includes/wc-account-functions.php +++ b/plugins/woocommerce/includes/wc-account-functions.php @@ -190,7 +190,13 @@ function wc_get_account_endpoint_url( $endpoint ) { * @return array */ function wc_get_account_orders_columns() { - $columns = apply_filters( + /** + * Filters the array of My Account > Orders columns. + * + * @since 2.6.0 + * @param array $columns Array of column labels keyed by column IDs. + */ + return apply_filters( 'woocommerce_account_orders_columns', array( 'order-number' => __( 'Order', 'woocommerce' ), @@ -200,9 +206,6 @@ function wc_get_account_orders_columns() { 'order-actions' => __( 'Actions', 'woocommerce' ), ) ); - - // Deprecated filter since 2.6.0. - return apply_filters( 'woocommerce_my_account_my_orders_columns', $columns ); } /** From f5bdbbaa202b8baf03c242184cf22834d4513c58 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Tue, 31 Jan 2023 17:11:27 -0800 Subject: [PATCH 132/228] Add i18n file for units --- plugins/woocommerce/i18n/units.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 plugins/woocommerce/i18n/units.php diff --git a/plugins/woocommerce/i18n/units.php b/plugins/woocommerce/i18n/units.php new file mode 100644 index 00000000000..1bf2a712542 --- /dev/null +++ b/plugins/woocommerce/i18n/units.php @@ -0,0 +1,28 @@ + 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' ), + ), + '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' ), + ), +); From 21bae97e82469e2c25a5c93e1b4c955453fc6cff Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Tue, 31 Jan 2023 17:12:11 -0800 Subject: [PATCH 133/228] Add I18nUtil class --- .../woocommerce/src/Utilities/I18nUtil.php | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 plugins/woocommerce/src/Utilities/I18nUtil.php diff --git a/plugins/woocommerce/src/Utilities/I18nUtil.php b/plugins/woocommerce/src/Utilities/I18nUtil.php new file mode 100644 index 00000000000..428790d76b2 --- /dev/null +++ b/plugins/woocommerce/src/Utilities/I18nUtil.php @@ -0,0 +1,55 @@ +plugin_path() . '/i18n/units.php'; + } + + $label = ''; + + if ( ! empty( self::$units['weight'][ $weight_unit ] ) ) { + $label = self::$units['weight'][ $weight_unit ]; + } + + return $label; + } + + /** + * Get the translated label for a dimensions unit of measure. + * + * @param string $dimensions_unit + * + * @return string + */ + public static function get_dimensions_unit_label( $dimensions_unit ) { + if ( empty( self::$units ) ) { + self::$units = include WC()->plugin_path() . '/i18n/units.php'; + } + + $label = ''; + + if ( ! empty( self::$units['dimensions'][ $dimensions_unit ] ) ) { + $label = self::$units['dimensions'][ $dimensions_unit ]; + } + + return $label; + } +} From 45069b1bf789e75e627c3f17b97cd63b5e4d6a29 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Tue, 31 Jan 2023 17:14:07 -0800 Subject: [PATCH 134/228] Update weight/dim formatting to use translated labels --- .../includes/wc-formatting-functions.php | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce/includes/wc-formatting-functions.php b/plugins/woocommerce/includes/wc-formatting-functions.php index 9f05e82c76f..a83caeaf519 100644 --- a/plugins/woocommerce/includes/wc-formatting-functions.php +++ b/plugins/woocommerce/includes/wc-formatting-functions.php @@ -8,6 +8,7 @@ * @version 2.1.0 */ +use Automattic\WooCommerce\Utilities\I18nUtil; use Automattic\WooCommerce\Utilities\NumberUtil; defined( 'ABSPATH' ) || exit; @@ -1303,7 +1304,14 @@ function wc_format_weight( $weight ) { $weight_string = wc_format_localized_decimal( $weight ); if ( ! empty( $weight_string ) ) { - $weight_string .= ' ' . get_option( 'woocommerce_weight_unit' ); + $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 + _x( '%1$s %2$s', 'formatted weight', 'woocommerce' ), + $weight_string, + $weight_label + ); } else { $weight_string = __( 'N/A', 'woocommerce' ); } @@ -1322,7 +1330,14 @@ function wc_format_dimensions( $dimensions ) { $dimension_string = implode( ' × ', array_filter( array_map( 'wc_format_localized_decimal', $dimensions ) ) ); if ( ! empty( $dimension_string ) ) { - $dimension_string .= ' ' . get_option( 'woocommerce_dimension_unit' ); + $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 + _x( '%1$s %2$s', 'formatted dimensions', 'woocommerce' ), + $dimension_string, + $dimension_label + ); } else { $dimension_string = __( 'N/A', 'woocommerce' ); } From 31d5b454438af2748813c44e920723f832acd972 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Tue, 31 Jan 2023 17:16:32 -0800 Subject: [PATCH 135/228] Add changelog file --- plugins/woocommerce/changelog/fix-31486-unit-i18n | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-31486-unit-i18n diff --git a/plugins/woocommerce/changelog/fix-31486-unit-i18n b/plugins/woocommerce/changelog/fix-31486-unit-i18n new file mode 100644 index 00000000000..868be37f223 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-31486-unit-i18n @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Translate the labels for units of measure. From cdff039569b915139fd1d54aed02e68038ee13b7 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Tue, 31 Jan 2023 17:26:14 -0800 Subject: [PATCH 136/228] Update products settings to use I18nUtil for unit labels --- .../settings/class-wc-settings-products.php | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/plugins/woocommerce/includes/admin/settings/class-wc-settings-products.php b/plugins/woocommerce/includes/admin/settings/class-wc-settings-products.php index 0f9751e31c9..5fe4f46d5d8 100644 --- a/plugins/woocommerce/includes/admin/settings/class-wc-settings-products.php +++ b/plugins/woocommerce/includes/admin/settings/class-wc-settings-products.php @@ -6,6 +6,8 @@ * @version 2.4.0 */ +use Automattic\WooCommerce\Utilities\I18nUtil; + if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -112,10 +114,10 @@ class WC_Settings_Products extends WC_Settings_Page { 'default' => 'kg', 'type' => 'select', 'options' => array( - 'kg' => __( 'kg', 'woocommerce' ), - 'g' => __( 'g', 'woocommerce' ), - 'lbs' => __( 'lbs', 'woocommerce' ), - 'oz' => __( 'oz', 'woocommerce' ), + 'kg' => I18nUtil::get_weight_unit_label( 'kg' ), + 'g' => I18nUtil::get_weight_unit_label( 'g' ), + 'lbs' => I18nUtil::get_weight_unit_label( 'lbs' ), + 'oz' => I18nUtil::get_weight_unit_label( 'oz' ), ), 'desc_tip' => true, ), @@ -129,11 +131,11 @@ class WC_Settings_Products extends WC_Settings_Page { 'default' => 'cm', 'type' => 'select', 'options' => array( - 'm' => __( 'm', 'woocommerce' ), - 'cm' => __( 'cm', 'woocommerce' ), - 'mm' => __( 'mm', 'woocommerce' ), - 'in' => __( 'in', 'woocommerce' ), - 'yd' => __( 'yd', 'woocommerce' ), + 'm' => I18nUtil::get_dimensions_unit_label( 'm' ), + 'cm' => I18nUtil::get_dimensions_unit_label( 'cm' ), + 'mm' => I18nUtil::get_dimensions_unit_label( 'mm' ), + 'in' => I18nUtil::get_dimensions_unit_label( 'in' ), + 'yd' => I18nUtil::get_dimensions_unit_label( 'yd' ), ), 'desc_tip' => true, ), From e4a18edf640a01b9bbaaa2eac46350ae3fcf3422 Mon Sep 17 00:00:00 2001 From: Sam Seay Date: Thu, 2 Feb 2023 04:44:37 +1300 Subject: [PATCH 137/228] Update turborepo to 1.7.0 (#36677) Update turbo to 1.7.0 --- package.json | 2 +- pnpm-lock.yaml | 159 ++++++++++++------------------------------------- 2 files changed, 38 insertions(+), 123 deletions(-) diff --git a/package.json b/package.json index 1e35a659206..54868064a86 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "sass": "^1.49.9", "sass-loader": "^10.2.1", "syncpack": "^8.3.9", - "turbo": "^1.4.5", + "turbo": "^1.7.0", "typescript": "^4.8.3", "url-loader": "^1.1.2", "webpack": "^5.70.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f668e27b27..63527c397d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,7 +34,7 @@ importers: sass: ^1.49.9 sass-loader: ^10.2.1 syncpack: ^8.3.9 - turbo: ^1.4.5 + turbo: ^1.7.0 typescript: ^4.8.3 url-loader: ^1.1.2 webpack: ^5.70.0 @@ -72,7 +72,7 @@ importers: sass: 1.49.9 sass-loader: 10.2.1_sass@1.49.9+webpack@5.70.0 syncpack: 8.3.9 - turbo: 1.4.5 + turbo: 1.7.0 typescript: 4.8.4 url-loader: 1.1.2_webpack@5.70.0 webpack: 5.70.0 @@ -6375,9 +6375,9 @@ packages: '@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-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.16.12 + '@babel/helper-module-imports': 7.16.0 + '@babel/helper-plugin-utils': 7.14.5 + babel-plugin-polyfill-corejs2: 0.3.0_@babel+core@7.16.12 babel-plugin-polyfill-corejs3: 0.4.0_@babel+core@7.16.12 babel-plugin-polyfill-regenerator: 0.3.0_@babel+core@7.16.12 semver: 6.3.0 @@ -6392,9 +6392,9 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.19.0 - babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.17.8 + '@babel/helper-module-imports': 7.16.0 + '@babel/helper-plugin-utils': 7.14.5 + babel-plugin-polyfill-corejs2: 0.3.0_@babel+core@7.17.8 babel-plugin-polyfill-corejs3: 0.4.0_@babel+core@7.17.8 babel-plugin-polyfill-regenerator: 0.3.0_@babel+core@7.17.8 semver: 6.3.0 @@ -7306,8 +7306,8 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-validator-option': 7.18.6 + '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-validator-option': 7.16.7 '@babel/plugin-transform-typescript': 7.16.8_@babel+core@7.16.12 transitivePeerDependencies: - supports-color @@ -18980,19 +18980,6 @@ packages: - 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: - '@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.3_@babel+core@7.17.8: resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} peerDependencies: @@ -25193,7 +25180,7 @@ packages: vue-template-compiler: optional: true dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.16.7 '@types/json-schema': 7.0.9 chalk: 4.1.2 chokidar: 3.5.3 @@ -25204,7 +25191,7 @@ packages: memfs: 3.3.0 minimatch: 3.1.2 schema-utils: 2.7.0 - semver: 7.3.8 + semver: 7.3.5 tapable: 1.1.3 typescript: 4.8.4 webpack: 5.70.0 @@ -25224,7 +25211,7 @@ packages: vue-template-compiler: optional: true dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.16.7 '@types/json-schema': 7.0.9 chalk: 4.1.2 chokidar: 3.5.3 @@ -25236,7 +25223,7 @@ packages: memfs: 3.3.0 minimatch: 3.1.2 schema-utils: 2.7.0 - semver: 7.3.8 + semver: 7.3.5 tapable: 1.1.3 typescript: 4.8.4 webpack: 4.46.0_webpack-cli@3.3.12 @@ -25288,7 +25275,7 @@ packages: vue-template-compiler: optional: true dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.16.7 '@types/json-schema': 7.0.9 chalk: 4.1.2 chokidar: 3.5.3 @@ -25299,7 +25286,7 @@ packages: memfs: 3.3.0 minimatch: 3.1.2 schema-utils: 2.7.0 - semver: 7.3.8 + semver: 7.3.5 tapable: 1.1.3 typescript: 4.8.4 webpack: 4.46.0 @@ -38961,7 +38948,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 @@ -39547,140 +39534,68 @@ packages: engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} dev: false - /turbo-android-arm64/1.4.5: - resolution: {integrity: sha512-cKPJVyS1A2BBVbcH8XVeBArtEjHxioEm9zQa3Hv68usQOOFW+KOjH+0fGvjqMrWztLVFhE+npeVsnyu/6AmRew==} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - /turbo-combine-reducers/1.0.2: resolution: {integrity: sha512-gHbdMZlA6Ym6Ur5pSH/UWrNQMIM9IqTH6SoL1DbHpqEdQ8i+cFunSmSlFykPt0eGQwZ4d/XTHOl74H0/kFBVWw==} - /turbo-darwin-64/1.4.5: - resolution: {integrity: sha512-cK6LjkziSfopTznpfx3EdW/C11xFlK01tYxNj0XPoBW4vNb1zLsfrI/HIwp0SzdNLqzCBwOJBK0VB07Q1MmlxQ==} + /turbo-darwin-64/1.7.0: + resolution: {integrity: sha512-hSGAueSf5Ko8J67mpqjpt9FsP6ePn1nMcl7IVPoJq5dHsgX3anCP/BPlexJ502bNK+87DDyhQhJ/LPSJXKrSYQ==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64/1.4.5: - resolution: {integrity: sha512-hnixteVmfllup0//mGr2AZOff8oJ9dEydRU/EvbyIJ7PdkSct8758YnP5l2yMyxxipHHALSwchOKcySoaPBLRw==} + /turbo-darwin-arm64/1.7.0: + resolution: {integrity: sha512-BLLOW5W6VZxk5+0ZOj5AO1qjM0P5isIgjbEuyAl8lHZ4s9antUbY4CtFrspT32XxPTYoDl4UjviPMcSsbcl3WQ==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-freebsd-64/1.4.5: - resolution: {integrity: sha512-DDNSDiKJF/F1qbSNlvRs2UO79iMPlKFw/ZcVCDKXRjMI5uMRujMIfNnoStAg6kifYZJ0KIxnzdsbbJFtCTgPhQ==} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /turbo-freebsd-arm64/1.4.5: - resolution: {integrity: sha512-pFU8ujUp7XU527NM04P4foDjOKfi8f0rNJqREU6AMxawiMI6y0oyHc3W4VNKm0Y9IsjcfguZKZRMPeQ0n7LgJA==} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /turbo-linux-32/1.4.5: - resolution: {integrity: sha512-Y5pnIetm4CwxbG1YQbvnTmjROPjQjX03ktHvmoekBleU1coYGShGPd9iarQZ96XkeaRevfwfSJ90AnDGwc3/QQ==} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /turbo-linux-64/1.4.5: - resolution: {integrity: sha512-tiTusM7mYib3iLmVOWmxhsg6xU3EArjOL4l3yh9rYBdArhDJk+DrhXkbJkYOYgNSWUEfPeBs0lzSkfTIQ2H21g==} + /turbo-linux-64/1.7.0: + resolution: {integrity: sha512-aw2qxmfZa+kT87SB3GNUoFimqEPzTlzlRqhPgHuAAT6Uf0JHnmebPt4K+ZPtDNl5yfVmtB05bhHPqw+5QV97Yg==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm/1.4.5: - resolution: {integrity: sha512-5BsTRsoZeUWXWau4t4CNdL6NjlDdXrh8suhj5YolXUVCe039A5IutS7Dgd4i8dV1BovFCU3bz38dqQI2Qst5eQ==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /turbo-linux-arm64/1.4.5: - resolution: {integrity: sha512-R03HPqb0tJtqsGF4P3oPWsggdpTe9CZiBEtzATFpL7WVzm/Dsimy1Wcj07v6gCWdgi/mWmRt+OcZfGhnf7mibQ==} + /turbo-linux-arm64/1.7.0: + resolution: {integrity: sha512-AJEx2jX+zO5fQtJpO3r6uhTabj4oSA5ZhB7zTs/rwu/XqoydsvStA4X8NDW4poTbOjF7DcSHizqwi04tSMzpJw==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-mips64le/1.4.5: - resolution: {integrity: sha512-08+WhWCx2Bp6wKH8im4Z2iEBCgiWJLZbNbZ8YHxamwlIk1JsklyCCMikMSWb0GmpKi7EIjdmtPRQY2CyoBefpg==} - cpu: [mipsel] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /turbo-linux-ppc64le/1.4.5: - resolution: {integrity: sha512-feGjTOYcCbg12Y6JzL8nxaUOzjqqYtO6vtxrJy16yVCd2k2htd18iZ1kLjWKDZA94vdyW5lfyAGsAQik2eAC4A==} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /turbo-windows-32/1.4.5: - resolution: {integrity: sha512-O4tOnGmVJrR1JOKepuUvx/kqgaU6eMEjYUihJlvQmz9pTA/ecs8HrTGyXrabbmp5YdUmF2qsXaAcOvEuJaNPmQ==} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /turbo-windows-64/1.4.5: - resolution: {integrity: sha512-3W9gGq9h2szhestsoA1XqN9hV0wCRpGCEV6fbb3q9EaJY8K9Tff8By0mi+fE31OKHY4om+PHg595q7kybcyG/Q==} + /turbo-windows-64/1.7.0: + resolution: {integrity: sha512-ewj7PPv2uxqv0r31hgnBa3E5qwUu7eyVRP5M1gB/TJXfSHduU79gbxpKCyxIZv2fL/N2/3U7EPOQPSZxBAoljA==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64/1.4.5: - resolution: {integrity: sha512-/77f6OJM/4MqYEoCMER5MZTbfJZuVN22R9mc7iJDFOrNJrwvAWiAx98Fvo7WEVpMjGFUEq4Wi0UV5SpMAGU3bA==} + /turbo-windows-arm64/1.7.0: + resolution: {integrity: sha512-LzjOUzveWkvTD0jP8DBMYiAnYemmydsvqxdSmsUapHHJkl6wKZIOQNSO7pxsy+9XM/1/+0f9Y9F9ZNl5lePTEA==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo/1.4.5: - resolution: {integrity: sha512-imAyc1kZusjzMWY4RO572FWws8x6CoM5mX7D3qtMjdGuqOBNBhPTIaJ0LmQLu+xzuKIOlJ5m/2587CsHOJTdgw==} + /turbo/1.7.0: + resolution: {integrity: sha512-cwympNwQNnQZ/TffBd8yT0i0O10Cf/hlxccCYgUcwhcGEb9rDjE5thDbHoHw1hlJQUF/5ua7ERJe7Zr0lNE/ww==} hasBin: true requiresBuild: true optionalDependencies: - turbo-android-arm64: 1.4.5 - turbo-darwin-64: 1.4.5 - turbo-darwin-arm64: 1.4.5 - turbo-freebsd-64: 1.4.5 - turbo-freebsd-arm64: 1.4.5 - turbo-linux-32: 1.4.5 - turbo-linux-64: 1.4.5 - turbo-linux-arm: 1.4.5 - turbo-linux-arm64: 1.4.5 - turbo-linux-mips64le: 1.4.5 - turbo-linux-ppc64le: 1.4.5 - turbo-windows-32: 1.4.5 - turbo-windows-64: 1.4.5 - turbo-windows-arm64: 1.4.5 + turbo-darwin-64: 1.7.0 + turbo-darwin-arm64: 1.7.0 + turbo-linux-64: 1.7.0 + turbo-linux-arm64: 1.7.0 + turbo-windows-64: 1.7.0 + turbo-windows-arm64: 1.7.0 dev: true /tweetnacl/0.14.5: From 5570f6a3e802356230fba071b0c1b86d676c9528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maikel=20David=20P=C3=A9rez=20G=C3=B3mez?= Date: Wed, 1 Feb 2023 14:04:50 -0300 Subject: [PATCH 138/228] Add tree-control expand/collapse on click the expander button or by a custom logic (#36434) * Add tree-control expand/collapse on click the expander button or by a custom logic * Update changelog * Clean up styles * Update isItemExpanded code doc * Rename expanded props * Destructure expander * Update shouldItemBeExpanded code doc * Remove useCallback from story * Remove useCallback from shouldItemBeExpanded code doc --------- Co-authored-by: Matt Sherman --- .../changelog/add-35851-tree-control-expander | 4 +++ .../hooks/use-expander.ts | 31 ++++++++++++++++ .../hooks/use-tree-item.ts | 15 +++++++- .../hooks/use-tree.ts | 9 ++++- .../stories/index.tsx | 35 +++++++++++++++++-- .../experimental-tree-control/tree-item.scss | 8 +++++ .../experimental-tree-control/tree-item.tsx | 29 +++++++++++++-- .../src/experimental-tree-control/types.ts | 16 +++++++++ 8 files changed, 140 insertions(+), 7 deletions(-) create mode 100644 packages/js/components/changelog/add-35851-tree-control-expander create mode 100644 packages/js/components/src/experimental-tree-control/hooks/use-expander.ts diff --git a/packages/js/components/changelog/add-35851-tree-control-expander b/packages/js/components/changelog/add-35851-tree-control-expander new file mode 100644 index 00000000000..943ea1225d5 --- /dev/null +++ b/packages/js/components/changelog/add-35851-tree-control-expander @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Add TreeControl expand/collapse functionality. diff --git a/packages/js/components/src/experimental-tree-control/hooks/use-expander.ts b/packages/js/components/src/experimental-tree-control/hooks/use-expander.ts new file mode 100644 index 00000000000..49ac33379cf --- /dev/null +++ b/packages/js/components/src/experimental-tree-control/hooks/use-expander.ts @@ -0,0 +1,31 @@ +/** + * External dependencies + */ +import { useEffect, useState } from 'react'; + +/** + * Internal dependencies + */ +import { TreeItemProps } from '../types'; + +export function useExpander( { + shouldItemBeExpanded, + item, +}: Pick< TreeItemProps, 'shouldItemBeExpanded' | 'item' > ) { + const [ isExpanded, setExpanded ] = useState( false ); + + useEffect( () => { + if ( + item.children?.length && + typeof shouldItemBeExpanded === 'function' + ) { + setExpanded( shouldItemBeExpanded( item ) ); + } + }, [ item, shouldItemBeExpanded ] ); + + function onToggleExpand() { + setExpanded( ( prev ) => ! prev ); + } + + return { isExpanded, onToggleExpand }; +} diff --git a/packages/js/components/src/experimental-tree-control/hooks/use-tree-item.ts b/packages/js/components/src/experimental-tree-control/hooks/use-tree-item.ts index f8aa5ade531..23fe64d2c13 100644 --- a/packages/js/components/src/experimental-tree-control/hooks/use-tree-item.ts +++ b/packages/js/components/src/experimental-tree-control/hooks/use-tree-item.ts @@ -7,13 +7,25 @@ import React from 'react'; * Internal dependencies */ import { TreeItemProps } from '../types'; +import { useExpander } from './use-expander'; -export function useTreeItem( { item, level, ...props }: TreeItemProps ) { +export function useTreeItem( { + item, + level, + shouldItemBeExpanded, + ...props +}: TreeItemProps ) { const nextLevel = level + 1; + const expander = useExpander( { + item, + shouldItemBeExpanded, + } ); + return { item, level: nextLevel, + expander, treeItemProps: { ...props, }, @@ -25,6 +37,7 @@ export function useTreeItem( { item, level, ...props }: TreeItemProps ) { treeProps: { items: item.children, level: nextLevel, + shouldItemBeExpanded, }, }; } diff --git a/packages/js/components/src/experimental-tree-control/hooks/use-tree.ts b/packages/js/components/src/experimental-tree-control/hooks/use-tree.ts index 2ab6b889c58..028a338a375 100644 --- a/packages/js/components/src/experimental-tree-control/hooks/use-tree.ts +++ b/packages/js/components/src/experimental-tree-control/hooks/use-tree.ts @@ -7,7 +7,13 @@ */ import { TreeProps } from '../types'; -export function useTree( { ref, items, level = 1, ...props }: TreeProps ) { +export function useTree( { + ref, + items, + level = 1, + shouldItemBeExpanded, + ...props +}: TreeProps ) { return { level, items, @@ -16,6 +22,7 @@ export function useTree( { ref, items, level = 1, ...props }: TreeProps ) { }, treeItemProps: { level, + shouldItemBeExpanded, }, }; } diff --git a/packages/js/components/src/experimental-tree-control/stories/index.tsx b/packages/js/components/src/experimental-tree-control/stories/index.tsx index d62a8ed1964..fdfcf2aee36 100644 --- a/packages/js/components/src/experimental-tree-control/stories/index.tsx +++ b/packages/js/components/src/experimental-tree-control/stories/index.tsx @@ -1,14 +1,14 @@ /** * External dependencies */ -import { BaseControl } from '@wordpress/components'; -import React, { createElement } from 'react'; +import { BaseControl, TextControl } from '@wordpress/components'; +import React, { createElement, useCallback, useState } from 'react'; /** * Internal dependencies */ import { TreeControl } from '../tree-control'; -import { Item } from '../types'; +import { Item, LinkedTree } from '../types'; const listItems: Item[] = [ { value: '1', label: 'Technology' }, @@ -36,6 +36,35 @@ export const SimpleTree: React.FC = () => { ); }; +function shouldItemBeExpanded( item: LinkedTree, filter: string ) { + if ( ! filter || ! item.children?.length ) return false; + return item.children.some( ( child ) => { + if ( new RegExp( filter, 'ig' ).test( child.data.label ) ) { + return true; + } + return shouldItemBeExpanded( child, filter ); + } ); +} + +export const ExpandOnFilter: React.FC = () => { + const [ filter, setFilter ] = useState( '' ); + + return ( + <> + + + + shouldItemBeExpanded( item, filter ) + } + /> + + + ); +}; + export default { title: 'WooCommerce Admin/experimental/TreeControl', component: TreeControl, 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 e98a713f4e4..72579e8757d 100644 --- a/packages/js/components/src/experimental-tree-control/tree-item.scss +++ b/packages/js/components/src/experimental-tree-control/tree-item.scss @@ -31,4 +31,12 @@ display: block; } } + &__expander { + display: flex; + align-items: center; + + .components-button { + padding: 0; + } + } } diff --git a/packages/js/components/src/experimental-tree-control/tree-item.tsx b/packages/js/components/src/experimental-tree-control/tree-item.tsx index e8e7c407932..17c4d670c63 100644 --- a/packages/js/components/src/experimental-tree-control/tree-item.tsx +++ b/packages/js/components/src/experimental-tree-control/tree-item.tsx @@ -1,7 +1,9 @@ /** * External dependencies */ +import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import { chevronDown, chevronUp } from '@wordpress/icons'; import classNames from 'classnames'; import { createElement, forwardRef } from 'react'; @@ -16,7 +18,13 @@ export const TreeItem = forwardRef( function ForwardedTreeItem( props: TreeItemProps, ref: React.ForwardedRef< HTMLLIElement > ) { - const { item, treeItemProps, headingProps, treeProps } = useTreeItem( { + const { + item, + treeItemProps, + headingProps, + treeProps, + expander: { isExpanded, onToggleExpand }, + } = useTreeItem( { ...props, ref, } ); @@ -36,9 +44,26 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
{ item.data.label }
+ + { Boolean( item.children?.length ) && ( +
+
+ ) }
- { Boolean( item.children.length ) && } + { Boolean( item.children.length ) && isExpanded && ( + + ) } ); } ); diff --git a/packages/js/components/src/experimental-tree-control/types.ts b/packages/js/components/src/experimental-tree-control/types.ts index c925bbb8574..e7487fcfe4d 100644 --- a/packages/js/components/src/experimental-tree-control/types.ts +++ b/packages/js/components/src/experimental-tree-control/types.ts @@ -16,6 +16,21 @@ export type TreeProps = React.DetailedHTMLProps< > & { level?: number; items: LinkedTree[]; + /** + * Return if the tree item passed in should be expanded. + * + * @example + * checkExpanded( item, filter ) + * } + * /> + * + * @param item The tree item to determine if should be expanded. + * + * @see {@link LinkedTree} + */ + shouldItemBeExpanded?( item: LinkedTree ): boolean; }; export type TreeItemProps = React.DetailedHTMLProps< @@ -24,6 +39,7 @@ export type TreeItemProps = React.DetailedHTMLProps< > & { level: number; item: LinkedTree; + shouldItemBeExpanded?( item: LinkedTree ): boolean; }; export type TreeControlProps = Omit< TreeProps, 'items' | 'level' > & { From 463082fb28cd976490e9bfe08f5ad87685bc3cee Mon Sep 17 00:00:00 2001 From: Barry Hughes <3594411+barryhughes@users.noreply.github.com> Date: Wed, 1 Feb 2023 11:59:42 -0800 Subject: [PATCH 139/228] Revert/36294 (#36714) * Revert "Update spelling of Cancelled to Canceled for US English. (#36294)" This reverts commit e588c560bfad0e02b49aed942b5ffb330673eb9c. * Changelog note --- packages/js/components/changelog/canceled | 4 ---- .../specs/merchant/wp-admin-order-status-filters.test.js | 2 +- plugins/woocommerce/changelog/revert-36294 | 5 +++++ .../admin/settings/class-wc-settings-accounts.php | 4 ++-- plugins/woocommerce/includes/class-wc-post-types.php | 4 ++-- .../woocommerce/includes/class-wc-regenerate-images.php | 2 +- .../includes/emails/class-wc-email-cancelled-order.php | 6 +++--- .../emails/class-wc-email-customer-on-hold-order.php | 2 +- plugins/woocommerce/includes/wc-order-functions.php | 2 +- .../api-core-tests/tests/reports/reports-crud.test.js | 2 +- .../api-core-tests/tests/settings/settings-crud.test.js | 8 ++++---- .../e2e-pw/tests/merchant/order-status-filter.spec.js | 2 +- .../unit-tests/order/class-wc-tests-order-functions.php | 2 +- 13 files changed, 23 insertions(+), 22 deletions(-) delete mode 100644 packages/js/components/changelog/canceled create mode 100644 plugins/woocommerce/changelog/revert-36294 diff --git a/packages/js/components/changelog/canceled b/packages/js/components/changelog/canceled deleted file mode 100644 index 2f8b5c512ca..00000000000 --- a/packages/js/components/changelog/canceled +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: tweak - -Update spelling of Cancelled to Canceled for US English. diff --git a/packages/js/e2e-core-tests/src/specs/merchant/wp-admin-order-status-filters.test.js b/packages/js/e2e-core-tests/src/specs/merchant/wp-admin-order-status-filters.test.js index 5090f917a9c..91a2451487b 100644 --- a/packages/js/e2e-core-tests/src/specs/merchant/wp-admin-order-status-filters.test.js +++ b/packages/js/e2e-core-tests/src/specs/merchant/wp-admin-order-status-filters.test.js @@ -16,7 +16,7 @@ const orderStatus = [ [ 'Processing', 'wc-processing' ], [ 'On hold', 'wc-on-hold' ], [ 'Completed', 'wc-completed' ], - [ 'Canceled', 'wc-cancelled' ], + [ 'Cancelled', 'wc-cancelled' ], [ 'Refunded', 'wc-refunded' ], [ 'Failed', 'wc-failed' ], ]; diff --git a/plugins/woocommerce/changelog/revert-36294 b/plugins/woocommerce/changelog/revert-36294 new file mode 100644 index 00000000000..ac985b65a44 --- /dev/null +++ b/plugins/woocommerce/changelog/revert-36294 @@ -0,0 +1,5 @@ +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. + + diff --git a/plugins/woocommerce/includes/admin/settings/class-wc-settings-accounts.php b/plugins/woocommerce/includes/admin/settings/class-wc-settings-accounts.php index 851bc7d2d46..be1c1dd71f7 100644 --- a/plugins/woocommerce/includes/admin/settings/class-wc-settings-accounts.php +++ b/plugins/woocommerce/includes/admin/settings/class-wc-settings-accounts.php @@ -203,8 +203,8 @@ class WC_Settings_Accounts extends WC_Settings_Page { 'autoload' => false, ), array( - 'title' => __( 'Retain canceled orders', 'woocommerce' ), - 'desc_tip' => __( 'Canceled orders are unpaid and may have been cancelled by the store owner or customer. They will be trashed after the specified duration.', 'woocommerce' ), + 'title' => __( 'Retain cancelled orders', 'woocommerce' ), + 'desc_tip' => __( 'Cancelled orders are unpaid and may have been cancelled by the store owner or customer. They will be trashed after the specified duration.', 'woocommerce' ), 'id' => 'woocommerce_trash_cancelled_orders', 'type' => 'relative_date_selector', 'placeholder' => __( 'N/A', 'woocommerce' ), diff --git a/plugins/woocommerce/includes/class-wc-post-types.php b/plugins/woocommerce/includes/class-wc-post-types.php index 43fcb6c975b..1ccafa5e2d7 100644 --- a/plugins/woocommerce/includes/class-wc-post-types.php +++ b/plugins/woocommerce/includes/class-wc-post-types.php @@ -598,13 +598,13 @@ class WC_Post_Types { 'label_count' => _n_noop( 'Completed (%s)', 'Completed (%s)', 'woocommerce' ), ), 'wc-cancelled' => array( - 'label' => _x( 'Canceled', 'Order status', 'woocommerce' ), + 'label' => _x( 'Cancelled', 'Order status', 'woocommerce' ), 'public' => false, 'exclude_from_search' => false, 'show_in_admin_all_list' => true, 'show_in_admin_status_list' => true, /* translators: %s: number of orders */ - 'label_count' => _n_noop( 'Canceled (%s)', 'Canceled (%s)', 'woocommerce' ), + 'label_count' => _n_noop( 'Cancelled (%s)', 'Cancelled (%s)', 'woocommerce' ), ), 'wc-refunded' => array( 'label' => _x( 'Refunded', 'Order status', 'woocommerce' ), diff --git a/plugins/woocommerce/includes/class-wc-regenerate-images.php b/plugins/woocommerce/includes/class-wc-regenerate-images.php index c2db4f8a8e1..ca59231e7fb 100644 --- a/plugins/woocommerce/includes/class-wc-regenerate-images.php +++ b/plugins/woocommerce/includes/class-wc-regenerate-images.php @@ -158,7 +158,7 @@ class WC_Regenerate_Images { $log = wc_get_logger(); $log->info( - __( 'Canceled product image regeneration job.', 'woocommerce' ), + __( 'Cancelled product image regeneration job.', 'woocommerce' ), array( 'source' => 'wc-image-regeneration', ) diff --git a/plugins/woocommerce/includes/emails/class-wc-email-cancelled-order.php b/plugins/woocommerce/includes/emails/class-wc-email-cancelled-order.php index 7ed5a5fe221..eeb4d34c73e 100644 --- a/plugins/woocommerce/includes/emails/class-wc-email-cancelled-order.php +++ b/plugins/woocommerce/includes/emails/class-wc-email-cancelled-order.php @@ -28,8 +28,8 @@ if ( ! class_exists( 'WC_Email_Cancelled_Order', false ) ) : */ public function __construct() { $this->id = 'cancelled_order'; - $this->title = __( 'Canceled order', 'woocommerce' ); - $this->description = __( 'Canceled order emails are sent to chosen recipient(s) when orders have been marked canceled (if they were previously processing or on-hold).', 'woocommerce' ); + $this->title = __( 'Cancelled order', 'woocommerce' ); + $this->description = __( 'Cancelled order emails are sent to chosen recipient(s) when orders have been marked cancelled (if they were previously processing or on-hold).', 'woocommerce' ); $this->template_html = 'emails/admin-cancelled-order.php'; $this->template_plain = 'emails/plain/admin-cancelled-order.php'; $this->placeholders = array( @@ -66,7 +66,7 @@ if ( ! class_exists( 'WC_Email_Cancelled_Order', false ) ) : * @return string */ public function get_default_heading() { - return __( 'Order Canceled: #{order_number}', 'woocommerce' ); + return __( 'Order Cancelled: #{order_number}', 'woocommerce' ); } /** diff --git a/plugins/woocommerce/includes/emails/class-wc-email-customer-on-hold-order.php b/plugins/woocommerce/includes/emails/class-wc-email-customer-on-hold-order.php index bf7fd3487dc..8be3f17c09d 100644 --- a/plugins/woocommerce/includes/emails/class-wc-email-customer-on-hold-order.php +++ b/plugins/woocommerce/includes/emails/class-wc-email-customer-on-hold-order.php @@ -30,7 +30,7 @@ if ( ! class_exists( 'WC_Email_Customer_On_Hold_Order', false ) ) : $this->id = 'customer_on_hold_order'; $this->customer_email = true; $this->title = __( 'Order on-hold', 'woocommerce' ); - $this->description = __( 'This is an order notification sent to customers containing order details after an order is placed on-hold from Pending, Canceled or Failed order status.', 'woocommerce' ); + $this->description = __( 'This is an order notification sent to customers containing order details after an order is placed on-hold from Pending, Cancelled or Failed order status.', 'woocommerce' ); $this->template_html = 'emails/customer-on-hold-order.php'; $this->template_plain = 'emails/plain/customer-on-hold-order.php'; $this->placeholders = array( diff --git a/plugins/woocommerce/includes/wc-order-functions.php b/plugins/woocommerce/includes/wc-order-functions.php index a385c21d5f2..7a40e589768 100644 --- a/plugins/woocommerce/includes/wc-order-functions.php +++ b/plugins/woocommerce/includes/wc-order-functions.php @@ -98,7 +98,7 @@ function wc_get_order_statuses() { 'wc-processing' => _x( 'Processing', 'Order status', 'woocommerce' ), 'wc-on-hold' => _x( 'On hold', 'Order status', 'woocommerce' ), 'wc-completed' => _x( 'Completed', 'Order status', 'woocommerce' ), - 'wc-cancelled' => _x( 'Canceled', 'Order status', 'woocommerce' ), + 'wc-cancelled' => _x( 'Cancelled', 'Order status', 'woocommerce' ), 'wc-refunded' => _x( 'Refunded', 'Order status', 'woocommerce' ), 'wc-failed' => _x( 'Failed', 'Order status', 'woocommerce' ), ); diff --git a/plugins/woocommerce/tests/api-core-tests/tests/reports/reports-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/reports/reports-crud.test.js index 40b284a2b8d..2766df5af22 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/reports/reports-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/reports/reports-crud.test.js @@ -281,7 +281,7 @@ test.describe('Reports API tests', () => { expect.arrayContaining([ expect.objectContaining({ "slug": "cancelled", - "name": "Canceled", + "name": "Cancelled", "total": expect.any(Number) }) ])); diff --git a/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js index a7922f15b88..b8d10686e3c 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js @@ -141,8 +141,8 @@ test.describe('Settings API tests: CRUD', () => { expect.arrayContaining([ expect.objectContaining({ id: "email_cancelled_order", - label: "Canceled order", - description: "Canceled order emails are sent to chosen recipient(s) when orders have been marked canceled (if they were previously processing or on-hold).", + label: "Cancelled order", + description: "Cancelled order emails are sent to chosen recipient(s) when orders have been marked cancelled (if they were previously processing or on-hold).", parent_id: "email", "sub_groups": expect.arrayContaining([]), }) @@ -162,7 +162,7 @@ test.describe('Settings API tests: CRUD', () => { expect.objectContaining({ id: "email_customer_on_hold_order", label: "Order on-hold", - description: "This is an order notification sent to customers containing order details after an order is placed on-hold from Pending, Canceled or Failed order status.", + description: "This is an order notification sent to customers containing order details after an order is placed on-hold from Pending, Cancelled or Failed order status.", parent_id: "email", "sub_groups": expect.arrayContaining([]), }) @@ -2549,4 +2549,4 @@ test.describe('Settings API tests: CRUD', () => { }); }); -}); +}); \ No newline at end of file diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-status-filter.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-status-filter.spec.js index 04dc5261e58..589a3bf08b9 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-status-filter.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-status-filter.spec.js @@ -10,7 +10,7 @@ const orderStatus = [ [ 'Processing', 'wc-processing' ], [ 'On hold', 'wc-on-hold' ], [ 'Completed', 'wc-completed' ], - [ 'Canceled', 'wc-cancelled' ], + [ 'Cancelled', 'wc-cancelled' ], [ 'Refunded', 'wc-refunded' ], [ 'Failed', 'wc-failed' ], ]; diff --git a/plugins/woocommerce/tests/legacy/unit-tests/order/class-wc-tests-order-functions.php b/plugins/woocommerce/tests/legacy/unit-tests/order/class-wc-tests-order-functions.php index 4df224a5e58..435020af779 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/order/class-wc-tests-order-functions.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/order/class-wc-tests-order-functions.php @@ -29,7 +29,7 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case { 'wc-processing' => _x( 'Processing', 'Order status', 'woocommerce' ), 'wc-on-hold' => _x( 'On hold', 'Order status', 'woocommerce' ), 'wc-completed' => _x( 'Completed', 'Order status', 'woocommerce' ), - 'wc-cancelled' => _x( 'Canceled', 'Order status', 'woocommerce' ), + 'wc-cancelled' => _x( 'Cancelled', 'Order status', 'woocommerce' ), 'wc-refunded' => _x( 'Refunded', 'Order status', 'woocommerce' ), 'wc-failed' => _x( 'Failed', 'Order status', 'woocommerce' ), ) From d0a2b582fe2da5cf628ace4f516eb7b18240372b Mon Sep 17 00:00:00 2001 From: Leif Singer Date: Thu, 2 Feb 2023 11:25:04 +0100 Subject: [PATCH 140/228] Consolidate eslint versions (#36700) * pin eslint in .syncpackrc run `pnpm run sync-dependencies` to ensure pinned version is used across projects, then run `pnpm install` to update `pnpm-lock.yaml` * add changelog files ran `pnpm run --no-bail --filter='[HEAD^1]' changelog add --significance=patch --type=dev --entry='Update eslint to 8.32.0 across the monorepo.'` * re-run `pnpm install` to fix what broke while merging --- .syncpackrc | 14 +- .../changelog/dev-consolidate-eslint-versions | 4 + packages/js/admin-e2e-tests/package.json | 2 +- packages/js/api-core-tests/package.json | 2 +- .../changelog/dev-consolidate-eslint-versions | 4 + packages/js/api/package.json | 2 +- .../changelog/dev-consolidate-eslint-versions | 4 + packages/js/components/package.json | 2 +- .../changelog/dev-consolidate-eslint-versions | 4 + packages/js/csv-export/package.json | 2 +- .../changelog/dev-consolidate-eslint-versions | 4 + packages/js/currency/package.json | 2 +- .../changelog/dev-consolidate-eslint-versions | 4 + .../js/customer-effort-score/package.json | 2 +- .../changelog/dev-consolidate-eslint-versions | 4 + packages/js/data/package.json | 2 +- .../changelog/dev-consolidate-eslint-versions | 4 + packages/js/date/package.json | 2 +- .../changelog/dev-consolidate-eslint-versions | 4 + .../package.json | 2 +- packages/js/e2e-core-tests/package.json | 2 +- packages/js/e2e-utils/package.json | 2 +- .../changelog/dev-consolidate-eslint-versions | 4 + packages/js/eslint-plugin/package.json | 2 +- .../changelog/dev-consolidate-eslint-versions | 4 + packages/js/experimental/package.json | 2 +- .../changelog/dev-consolidate-eslint-versions | 4 + packages/js/explat/package.json | 2 +- packages/js/internal-e2e-builds/package.json | 2 +- packages/js/internal-js-tests/package.json | 2 +- packages/js/internal-style-build/package.json | 2 +- .../changelog/dev-consolidate-eslint-versions | 4 + packages/js/navigation/package.json | 2 +- packages/js/notices/package.json | 2 +- .../changelog/dev-consolidate-eslint-versions | 4 + packages/js/number/package.json | 2 +- .../changelog/dev-consolidate-eslint-versions | 4 + packages/js/onboarding/package.json | 2 +- .../changelog/dev-consolidate-eslint-versions | 4 + packages/js/product-editor/package.json | 2 +- .../changelog/dev-consolidate-eslint-versions | 4 + packages/js/tracks/package.json | 2 +- plugins/woocommerce-admin/package.json | 2 +- .../changelog/dev-consolidate-eslint-versions | 4 + plugins/woocommerce-beta-tester/package.json | 2 +- .../changelog/dev-consolidate-eslint-versions | 4 + plugins/woocommerce/package.json | 2 +- pnpm-lock.yaml | 2224 +++++------------ tools/code-analyzer/package.json | 2 +- tools/monorepo-merge/package.json | 2 +- 50 files changed, 770 insertions(+), 1602 deletions(-) create mode 100644 packages/js/admin-e2e-tests/changelog/dev-consolidate-eslint-versions create mode 100644 packages/js/api/changelog/dev-consolidate-eslint-versions create mode 100644 packages/js/components/changelog/dev-consolidate-eslint-versions create mode 100644 packages/js/csv-export/changelog/dev-consolidate-eslint-versions create mode 100644 packages/js/currency/changelog/dev-consolidate-eslint-versions create mode 100644 packages/js/customer-effort-score/changelog/dev-consolidate-eslint-versions create mode 100644 packages/js/data/changelog/dev-consolidate-eslint-versions create mode 100644 packages/js/date/changelog/dev-consolidate-eslint-versions create mode 100644 packages/js/dependency-extraction-webpack-plugin/changelog/dev-consolidate-eslint-versions create mode 100644 packages/js/eslint-plugin/changelog/dev-consolidate-eslint-versions create mode 100644 packages/js/experimental/changelog/dev-consolidate-eslint-versions create mode 100644 packages/js/explat/changelog/dev-consolidate-eslint-versions create mode 100644 packages/js/navigation/changelog/dev-consolidate-eslint-versions create mode 100644 packages/js/number/changelog/dev-consolidate-eslint-versions create mode 100644 packages/js/onboarding/changelog/dev-consolidate-eslint-versions create mode 100644 packages/js/product-editor/changelog/dev-consolidate-eslint-versions create mode 100644 packages/js/tracks/changelog/dev-consolidate-eslint-versions create mode 100644 plugins/woocommerce-beta-tester/changelog/dev-consolidate-eslint-versions create mode 100644 plugins/woocommerce/changelog/dev-consolidate-eslint-versions diff --git a/.syncpackrc b/.syncpackrc index 4ec2e4ff05e..b2031900baf 100644 --- a/.syncpackrc +++ b/.syncpackrc @@ -1,6 +1,6 @@ { "dev": true, - "filter": "^(?:react|react-dom|typescript|@typescript-eslint|@types/react).*$", + "filter": "^(?:react|react-dom|eslint|typescript|@typescript-eslint|@types/react).*$", "indent": "\t", "overrides": true, "peer": true, @@ -51,6 +51,18 @@ "**" ], "pinVersion": "^4.8.3" + }, + { + "dependencies": [ + "eslint" + ], + "dependencyTypes": [ + "devDependencies" + ], + "packages": [ + "**" + ], + "pinVersion": "^8.32.0" } ] } diff --git a/packages/js/admin-e2e-tests/changelog/dev-consolidate-eslint-versions b/packages/js/admin-e2e-tests/changelog/dev-consolidate-eslint-versions new file mode 100644 index 00000000000..d3d95c39119 --- /dev/null +++ b/packages/js/admin-e2e-tests/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/admin-e2e-tests/package.json b/packages/js/admin-e2e-tests/package.json index 93926703248..df3e15d3f0d 100644 --- a/packages/js/admin-e2e-tests/package.json +++ b/packages/js/admin-e2e-tests/package.json @@ -44,7 +44,7 @@ "@typescript-eslint/eslint-plugin": "^5.43.0", "@woocommerce/api": "^0.2.0", "@woocommerce/eslint-plugin": "workspace:*", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "jest-mock-extended": "^1.0.18", diff --git a/packages/js/api-core-tests/package.json b/packages/js/api-core-tests/package.json index d176de4d7b6..aac5f23762e 100644 --- a/packages/js/api-core-tests/package.json +++ b/packages/js/api-core-tests/package.json @@ -36,7 +36,7 @@ }, "devDependencies": { "@woocommerce/eslint-plugin": "workspace:*", - "eslint": "^8.12.0" + "eslint": "^8.32.0" }, "publishConfig": { "access": "public" diff --git a/packages/js/api/changelog/dev-consolidate-eslint-versions b/packages/js/api/changelog/dev-consolidate-eslint-versions new file mode 100644 index 00000000000..d3d95c39119 --- /dev/null +++ b/packages/js/api/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/api/package.json b/packages/js/api/package.json index b88f65e0018..78562c19794 100644 --- a/packages/js/api/package.json +++ b/packages/js/api/package.json @@ -55,7 +55,7 @@ "@typescript-eslint/parser": "^5.43.0", "@woocommerce/eslint-plugin": "workspace:*", "axios-mock-adapter": "^1.20.0", - "eslint": "^8.2.0", + "eslint": "^8.32.0", "jest": "^27", "ts-jest": "^27", "typescript": "^4.8.3" diff --git a/packages/js/components/changelog/dev-consolidate-eslint-versions b/packages/js/components/changelog/dev-consolidate-eslint-versions new file mode 100644 index 00000000000..d3d95c39119 --- /dev/null +++ b/packages/js/components/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/components/package.json b/packages/js/components/package.json index 652c5409229..1f810a551cd 100644 --- a/packages/js/components/package.json +++ b/packages/js/components/package.json @@ -130,7 +130,7 @@ "@wordpress/scripts": "^12.6.1", "concurrently": "^7.0.0", "css-loader": "^3.6.0", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "postcss-loader": "^3.0.0", diff --git a/packages/js/csv-export/changelog/dev-consolidate-eslint-versions b/packages/js/csv-export/changelog/dev-consolidate-eslint-versions new file mode 100644 index 00000000000..d3d95c39119 --- /dev/null +++ b/packages/js/csv-export/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/csv-export/package.json b/packages/js/csv-export/package.json index 8d39249128f..fcae9f11c64 100644 --- a/packages/js/csv-export/package.json +++ b/packages/js/csv-export/package.json @@ -50,7 +50,7 @@ "@babel/core": "^7.17.5", "@types/jest": "^27.4.1", "@woocommerce/eslint-plugin": "workspace:*", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "rimraf": "^3.0.2", diff --git a/packages/js/currency/changelog/dev-consolidate-eslint-versions b/packages/js/currency/changelog/dev-consolidate-eslint-versions new file mode 100644 index 00000000000..d3d95c39119 --- /dev/null +++ b/packages/js/currency/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/currency/package.json b/packages/js/currency/package.json index 9a6f0dea0f2..c7be92bd4d8 100644 --- a/packages/js/currency/package.json +++ b/packages/js/currency/package.json @@ -53,7 +53,7 @@ "@babel/core": "^7.17.5", "@types/jest": "^27.4.1", "@woocommerce/eslint-plugin": "workspace:*", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "rimraf": "^3.0.2", diff --git a/packages/js/customer-effort-score/changelog/dev-consolidate-eslint-versions b/packages/js/customer-effort-score/changelog/dev-consolidate-eslint-versions new file mode 100644 index 00000000000..d3d95c39119 --- /dev/null +++ b/packages/js/customer-effort-score/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/customer-effort-score/package.json b/packages/js/customer-effort-score/package.json index 4b03da8ce39..45fbed8067d 100644 --- a/packages/js/customer-effort-score/package.json +++ b/packages/js/customer-effort-score/package.json @@ -52,7 +52,7 @@ "@wordpress/browserslist-config": "^4.1.1", "concurrently": "^7.0.0", "css-loader": "^3.6.0", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "postcss-loader": "^3.0.0", diff --git a/packages/js/data/changelog/dev-consolidate-eslint-versions b/packages/js/data/changelog/dev-consolidate-eslint-versions new file mode 100644 index 00000000000..d3d95c39119 --- /dev/null +++ b/packages/js/data/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/data/package.json b/packages/js/data/package.json index cd803090447..11b66eff132 100644 --- a/packages/js/data/package.json +++ b/packages/js/data/package.json @@ -63,7 +63,7 @@ "@types/wordpress__data": "^6.0.0", "@types/wordpress__data-controls": "^2.2.0", "@woocommerce/eslint-plugin": "workspace:*", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "redux": "^4.1.0", diff --git a/packages/js/date/changelog/dev-consolidate-eslint-versions b/packages/js/date/changelog/dev-consolidate-eslint-versions new file mode 100644 index 00000000000..d3d95c39119 --- /dev/null +++ b/packages/js/date/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/date/package.json b/packages/js/date/package.json index c1ed7a0a656..b9788612dfd 100644 --- a/packages/js/date/package.json +++ b/packages/js/date/package.json @@ -40,7 +40,7 @@ "@types/qs": "^6.9.7", "@woocommerce/eslint-plugin": "workspace:*", "d3-time-format": "^2.3.0", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "rimraf": "^3.0.2", diff --git a/packages/js/dependency-extraction-webpack-plugin/changelog/dev-consolidate-eslint-versions b/packages/js/dependency-extraction-webpack-plugin/changelog/dev-consolidate-eslint-versions new file mode 100644 index 00000000000..d3d95c39119 --- /dev/null +++ b/packages/js/dependency-extraction-webpack-plugin/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/dependency-extraction-webpack-plugin/package.json b/packages/js/dependency-extraction-webpack-plugin/package.json index 70884483c27..67bb2110c65 100644 --- a/packages/js/dependency-extraction-webpack-plugin/package.json +++ b/packages/js/dependency-extraction-webpack-plugin/package.json @@ -30,7 +30,7 @@ "devDependencies": { "@babel/core": "^7.17.5", "@woocommerce/eslint-plugin": "workspace:*", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "rimraf": "^3.0.2", diff --git a/packages/js/e2e-core-tests/package.json b/packages/js/e2e-core-tests/package.json index 45f836da3c0..70ef896ba73 100644 --- a/packages/js/e2e-core-tests/package.json +++ b/packages/js/e2e-core-tests/package.json @@ -41,7 +41,7 @@ "@wordpress/babel-plugin-import-jsx-pragma": "1.1.3", "@wordpress/babel-preset-default": "3.0.2", "@wordpress/browserslist-config": "^4.1.0", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "eslint-plugin-jest": "23.20.0" }, "peerDependencies": { diff --git a/packages/js/e2e-utils/package.json b/packages/js/e2e-utils/package.json index d643cdc4553..ebc1a883d91 100644 --- a/packages/js/e2e-utils/package.json +++ b/packages/js/e2e-utils/package.json @@ -37,7 +37,7 @@ "@wordpress/babel-plugin-import-jsx-pragma": "1.1.3", "@wordpress/babel-preset-default": "3.0.2", "@wordpress/browserslist-config": "^4.1.0", - "eslint": "^8.1.0", + "eslint": "^8.32.0", "eslint-plugin-jest": "23.20.0" }, "peerDependencies": { diff --git a/packages/js/eslint-plugin/changelog/dev-consolidate-eslint-versions b/packages/js/eslint-plugin/changelog/dev-consolidate-eslint-versions new file mode 100644 index 00000000000..d3d95c39119 --- /dev/null +++ b/packages/js/eslint-plugin/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/eslint-plugin/package.json b/packages/js/eslint-plugin/package.json index 13010b98cde..0ce2429a271 100644 --- a/packages/js/eslint-plugin/package.json +++ b/packages/js/eslint-plugin/package.json @@ -47,7 +47,7 @@ }, "devDependencies": { "@babel/core": "^7.17.5", - "eslint": "^8.25.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "rimraf": "^3.0.2", diff --git a/packages/js/experimental/changelog/dev-consolidate-eslint-versions b/packages/js/experimental/changelog/dev-consolidate-eslint-versions new file mode 100644 index 00000000000..d3d95c39119 --- /dev/null +++ b/packages/js/experimental/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/experimental/package.json b/packages/js/experimental/package.json index aeb5cfbc819..4c64d5baeee 100644 --- a/packages/js/experimental/package.json +++ b/packages/js/experimental/package.json @@ -65,7 +65,7 @@ "@wordpress/browserslist-config": "^4.1.1", "concurrently": "^7.0.0", "css-loader": "^3.6.0", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "postcss-loader": "^3.0.0", diff --git a/packages/js/explat/changelog/dev-consolidate-eslint-versions b/packages/js/explat/changelog/dev-consolidate-eslint-versions new file mode 100644 index 00000000000..d3d95c39119 --- /dev/null +++ b/packages/js/explat/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/explat/package.json b/packages/js/explat/package.json index 1546a3faded..507cf9d04ec 100644 --- a/packages/js/explat/package.json +++ b/packages/js/explat/package.json @@ -45,7 +45,7 @@ "@types/qs": "^6.9.7", "@types/react": "^17.0.2", "@woocommerce/eslint-plugin": "workspace:*", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "rimraf": "^3.0.2", diff --git a/packages/js/internal-e2e-builds/package.json b/packages/js/internal-e2e-builds/package.json index 2cc3331a78b..21622e605a7 100644 --- a/packages/js/internal-e2e-builds/package.json +++ b/packages/js/internal-e2e-builds/package.json @@ -28,7 +28,7 @@ "@babel/core": "7.12.9", "@woocommerce/eslint-plugin": "workspace:*", "chalk": "^4.1.2", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "glob": "^7.2.0", "lodash": "^4.17.21", "mkdirp": "^1.0.4" diff --git a/packages/js/internal-js-tests/package.json b/packages/js/internal-js-tests/package.json index 06313153f36..3375236d97c 100644 --- a/packages/js/internal-js-tests/package.json +++ b/packages/js/internal-js-tests/package.json @@ -40,7 +40,7 @@ "devDependencies": { "@babel/core": "^7.17.5", "@woocommerce/eslint-plugin": "workspace:*", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "rimraf": "^3.0.2", diff --git a/packages/js/internal-style-build/package.json b/packages/js/internal-style-build/package.json index 9c4302a2bdb..0227ed82d05 100644 --- a/packages/js/internal-style-build/package.json +++ b/packages/js/internal-style-build/package.json @@ -40,7 +40,7 @@ "devDependencies": { "@babel/core": "^7.17.5", "@woocommerce/eslint-plugin": "workspace:*", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "rimraf": "^3.0.2", diff --git a/packages/js/navigation/changelog/dev-consolidate-eslint-versions b/packages/js/navigation/changelog/dev-consolidate-eslint-versions new file mode 100644 index 00000000000..d3d95c39119 --- /dev/null +++ b/packages/js/navigation/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/navigation/package.json b/packages/js/navigation/package.json index b1f12fc1002..d68f59ef901 100644 --- a/packages/js/navigation/package.json +++ b/packages/js/navigation/package.json @@ -63,7 +63,7 @@ "@types/jest": "^27.4.1", "@types/qs": "^6.9.7", "@woocommerce/eslint-plugin": "workspace:*", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "rimraf": "^3.0.2", diff --git a/packages/js/notices/package.json b/packages/js/notices/package.json index 6c4252c1797..33abc61fa19 100644 --- a/packages/js/notices/package.json +++ b/packages/js/notices/package.json @@ -56,7 +56,7 @@ "@types/wordpress__data": "^6.0.0", "@types/wordpress__notices": "^3.5.0", "@woocommerce/eslint-plugin": "workspace:*", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "redux": "^4.2.0", diff --git a/packages/js/number/changelog/dev-consolidate-eslint-versions b/packages/js/number/changelog/dev-consolidate-eslint-versions new file mode 100644 index 00000000000..d3d95c39119 --- /dev/null +++ b/packages/js/number/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/number/package.json b/packages/js/number/package.json index 0cda95388c3..08026f03902 100644 --- a/packages/js/number/package.json +++ b/packages/js/number/package.json @@ -50,7 +50,7 @@ "@types/jest": "^27.4.1", "@types/lodash": "^4.14.184", "@woocommerce/eslint-plugin": "workspace:*", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "rimraf": "^3.0.2", diff --git a/packages/js/onboarding/changelog/dev-consolidate-eslint-versions b/packages/js/onboarding/changelog/dev-consolidate-eslint-versions new file mode 100644 index 00000000000..d3d95c39119 --- /dev/null +++ b/packages/js/onboarding/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/onboarding/package.json b/packages/js/onboarding/package.json index 62fdf5e3d01..96dd68e72dc 100644 --- a/packages/js/onboarding/package.json +++ b/packages/js/onboarding/package.json @@ -48,7 +48,7 @@ "@wordpress/browserslist-config": "^4.1.1", "@types/wordpress__components": "^19.10.1", "css-loader": "^3.6.0", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "postcss-loader": "^3.0.0", diff --git a/packages/js/product-editor/changelog/dev-consolidate-eslint-versions b/packages/js/product-editor/changelog/dev-consolidate-eslint-versions new file mode 100644 index 00000000000..d3d95c39119 --- /dev/null +++ b/packages/js/product-editor/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/product-editor/package.json b/packages/js/product-editor/package.json index 7e22fc72096..abb3c6d7361 100644 --- a/packages/js/product-editor/package.json +++ b/packages/js/product-editor/package.json @@ -38,7 +38,7 @@ "@woocommerce/internal-style-build": "workspace:*", "@wordpress/browserslist-config": "^4.1.1", "css-loader": "^3.6.0", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "postcss-loader": "^3.0.0", diff --git a/packages/js/tracks/changelog/dev-consolidate-eslint-versions b/packages/js/tracks/changelog/dev-consolidate-eslint-versions new file mode 100644 index 00000000000..d3d95c39119 --- /dev/null +++ b/packages/js/tracks/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/tracks/package.json b/packages/js/tracks/package.json index 50a20da3f11..a69bd0cb5d2 100644 --- a/packages/js/tracks/package.json +++ b/packages/js/tracks/package.json @@ -46,7 +46,7 @@ "@babel/core": "^7.17.5", "@types/debug": "^4.1.7", "@woocommerce/eslint-plugin": "workspace:*", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "rimraf": "^3.0.2", diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 49b527f4916..d04562a4d72 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -177,7 +177,7 @@ "copy-webpack-plugin": "^10.2.4", "cross-env": "^7.0.3", "css-loader": "^6.7.0", - "eslint": "^8.10.0", + "eslint": "^8.32.0", "eslint-import-resolver-typescript": "^2.5.0", "eslint-import-resolver-webpack": "^0.13.2", "eslint-plugin-import": "^2.25.4", diff --git a/plugins/woocommerce-beta-tester/changelog/dev-consolidate-eslint-versions b/plugins/woocommerce-beta-tester/changelog/dev-consolidate-eslint-versions new file mode 100644 index 00000000000..d3d95c39119 --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/plugins/woocommerce-beta-tester/package.json b/plugins/woocommerce-beta-tester/package.json index 46a60c72a85..fe5cdda91c4 100644 --- a/plugins/woocommerce-beta-tester/package.json +++ b/plugins/woocommerce-beta-tester/package.json @@ -18,7 +18,7 @@ "@wordpress/env": "^4.8.0", "@wordpress/prettier-config": "^2.5.0", "@wordpress/scripts": "^19.2.4", - "eslint": "5.16.0", + "eslint": "^8.32.0", "prettier": "npm:wp-prettier@^2.6.2", "ts-loader": "^9.4.1", "typescript": "^4.8.3", diff --git a/plugins/woocommerce/changelog/dev-consolidate-eslint-versions b/plugins/woocommerce/changelog/dev-consolidate-eslint-versions new file mode 100644 index 00000000000..d3d95c39119 --- /dev/null +++ b/plugins/woocommerce/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index 15588bfbaec..83a77b28d1e 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -77,7 +77,7 @@ "cross-env": "6.0.3", "deasync": "0.1.26", "dotenv": "^10.0.0", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "eslint-config-wpcalypso": "5.0.0", "eslint-plugin-jest": "23.20.0", "istanbul": "1.0.0-alpha.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63527c397d6..329e506d368 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,7 +51,7 @@ importers: '@types/node': 14.14.33 '@woocommerce/eslint-plugin': link:packages/js/eslint-plugin '@wordpress/data': 6.15.0_react@17.0.2 - '@wordpress/eslint-plugin': 11.1.0_mcybzr52q3u5poieijntti3gbe + '@wordpress/eslint-plugin': 11.1.0_sxmqrsrdaatoogkbdrkrjpdxyi '@wordpress/prettier-config': 1.1.1 babel-loader: 8.2.3_2p3p4wasefxeg63hu27rmsqfnq chalk: 4.1.2 @@ -91,7 +91,7 @@ importers: '@woocommerce/e2e-utils': workspace:* '@woocommerce/eslint-plugin': workspace:* config: ^3.3.7 - eslint: ^8.12.0 + eslint: ^8.32.0 jest: ^27.5.1 jest-cli: ^27.5.1 jest-mock-extended: ^1.0.18 @@ -111,10 +111,10 @@ importers: '@types/config': 0.0.41 '@types/expect-puppeteer': 4.4.7 '@types/puppeteer': 5.4.5 - '@typescript-eslint/eslint-plugin': 5.43.0_77yfnzmbnonej4k3inkgdy4i5u + '@typescript-eslint/eslint-plugin': 5.43.0_7f3f6qw7x62gbtvc33ujooybme '@woocommerce/api': link:../api '@woocommerce/eslint-plugin': link:../eslint-plugin - eslint: 8.12.0 + eslint: 8.32.0 jest: 27.5.1 jest-cli: 27.5.1 jest-mock-extended: 1.0.18_l3u6ka6x73lqye3ea4q6yelf2e @@ -133,7 +133,7 @@ importers: axios: ^0.24.0 axios-mock-adapter: ^1.20.0 create-hmac: 1.1.7 - eslint: ^8.2.0 + eslint: ^8.32.0 jest: ^27 oauth-1.0a: 2.2.6 ts-jest: ^27 @@ -146,11 +146,11 @@ importers: '@types/create-hmac': 1.1.0 '@types/jest': 27.4.1 '@types/node': 13.13.5 - '@typescript-eslint/eslint-plugin': 5.43.0_72qpevtmaezorvyo7j2xoo37sy - '@typescript-eslint/parser': 5.43.0_yd7pksmmyt33nzyuulu63alu3m + '@typescript-eslint/eslint-plugin': 5.43.0_7f3f6qw7x62gbtvc33ujooybme + '@typescript-eslint/parser': 5.43.0_yygwinqv3a2io74xmwofqb7uka '@woocommerce/eslint-plugin': link:../eslint-plugin axios-mock-adapter: 1.20.0_axios@0.24.0 - eslint: 8.2.0 + eslint: 8.32.0 jest: 27.5.1 ts-jest: 27.1.3_wfmhell6c5i72vvtgtvpmkkb6i typescript: 4.8.4 @@ -160,7 +160,7 @@ importers: '@woocommerce/eslint-plugin': workspace:* allure-commandline: ^2.17.2 dotenv: ^10.0.0 - eslint: ^8.12.0 + eslint: ^8.32.0 jest: ^27.5.1 jest-allure: ^0.1.3 jest-runner-groups: ^2.1.0 @@ -176,7 +176,7 @@ importers: supertest: 6.2.4 devDependencies: '@woocommerce/eslint-plugin': link:../eslint-plugin - eslint: 8.12.0 + eslint: 8.32.0 packages/js/components: specifiers: @@ -263,7 +263,7 @@ importers: dompurify: ^2.3.6 downshift: ^6.1.12 emoji-flags: ^1.3.0 - eslint: ^8.12.0 + eslint: ^8.32.0 gridicons: ^3.4.0 jest: ^27.5.1 jest-cli: ^27.5.1 @@ -347,15 +347,15 @@ importers: '@babel/runtime': 7.17.7 '@storybook/addon-actions': 6.4.19_hiunvzosbwliizyirxfy6hjyim '@storybook/addon-console': 1.2.3_kthckm6zfmobggl2ahqbjihlce - '@storybook/addon-controls': 6.4.19_3mpzmuykh5ctyyi3r2d2agoucu - '@storybook/addon-docs': 6.4.19_fzsnscfffpgd4jw4kgkdqz7wca + '@storybook/addon-controls': 6.4.19_wl7ffu5ts6ayqm24qlkw7h6j4e + '@storybook/addon-docs': 6.4.19_td5ldnqdrdzplltfsjeiin6b2q '@storybook/addon-knobs': 6.4.0_nu75ilgc3qugomjhuwv2hk42im '@storybook/addon-links': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/addons': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/api': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/components': 6.4.19_hiunvzosbwliizyirxfy6hjyim '@storybook/core-events': 6.4.19 - '@storybook/react': 6.4.19_oycjqkyefi4akx2twppuux3udq + '@storybook/react': 6.4.19_cqdgeqmmrux6joug3kc73q4l6m '@storybook/theming': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@testing-library/dom': 8.11.3 '@testing-library/jest-dom': 5.16.2 @@ -378,7 +378,7 @@ importers: '@wordpress/scripts': 12.6.1_h4xx42qb2l7ylq2u26dkj2fbyi concurrently: 7.0.0 css-loader: 3.6.0_webpack@5.70.0 - eslint: 8.12.0 + eslint: 8.32.0 jest: 27.5.1 jest-cli: 27.5.1 postcss-loader: 3.0.0 @@ -400,7 +400,7 @@ importers: '@types/jest': ^27.4.1 '@woocommerce/eslint-plugin': workspace:* browser-filesaver: ^1.1.1 - eslint: ^8.12.0 + eslint: ^8.32.0 jest: ^27.5.1 jest-cli: ^27.5.1 moment: ^2.29.1 @@ -414,7 +414,7 @@ importers: '@babel/core': 7.17.8 '@types/jest': 27.4.1 '@woocommerce/eslint-plugin': link:../eslint-plugin - eslint: 8.12.0 + eslint: 8.32.0 jest: 27.5.1 jest-cli: 27.5.1 rimraf: 3.0.2 @@ -431,7 +431,7 @@ importers: '@wordpress/element': ^4.1.1 '@wordpress/html-entities': ^3.3.1 '@wordpress/i18n': ^3.20.0 - eslint: ^8.12.0 + eslint: ^8.32.0 jest: ^27.5.1 jest-cli: ^27.5.1 rimraf: ^3.0.2 @@ -447,7 +447,7 @@ importers: '@babel/core': 7.17.8 '@types/jest': 27.4.1 '@woocommerce/eslint-plugin': link:../eslint-plugin - eslint: 8.12.0 + eslint: 8.32.0 jest: 27.5.1 jest-cli: 27.5.1 rimraf: 3.0.2 @@ -476,7 +476,7 @@ importers: classnames: ^2.3.1 concurrently: ^7.0.0 css-loader: ^3.6.0 - eslint: ^8.12.0 + eslint: ^8.32.0 jest: ^27.5.1 jest-cli: ^27.5.1 postcss-loader: ^3.0.0 @@ -516,7 +516,7 @@ importers: '@wordpress/browserslist-config': 4.1.2 concurrently: 7.0.0 css-loader: 3.6.0_webpack@5.70.0 - eslint: 8.12.0 + eslint: 8.32.0 jest: 27.5.1 jest-cli: 27.5.1 postcss-loader: 3.0.0 @@ -558,7 +558,7 @@ importers: '@wordpress/i18n': ^4.3.1 '@wordpress/url': ^3.4.1 dompurify: ^2.3.6 - eslint: ^8.12.0 + eslint: ^8.32.0 jest: ^27.5.1 jest-cli: ^27.5.1 md5: ^2.3.0 @@ -608,7 +608,7 @@ importers: '@types/wordpress__data': 6.0.0 '@types/wordpress__data-controls': 2.2.0 '@woocommerce/eslint-plugin': link:../eslint-plugin - eslint: 8.12.0 + eslint: 8.32.0 jest: 27.5.1 jest-cli: 27.5.1 redux: 4.1.2 @@ -627,7 +627,7 @@ importers: '@wordpress/date': ^4.3.1 '@wordpress/i18n': ^4.3.1 d3-time-format: ^2.3.0 - eslint: ^8.12.0 + eslint: ^8.32.0 jest: ^27.5.1 jest-cli: ^27.5.1 lodash: ^4.17.0 @@ -652,7 +652,7 @@ importers: '@types/qs': 6.9.7 '@woocommerce/eslint-plugin': link:../eslint-plugin d3-time-format: 2.3.0 - eslint: 8.12.0 + eslint: 8.32.0 jest: 27.5.1 jest-cli: 27.5.1 rimraf: 3.0.2 @@ -664,7 +664,7 @@ importers: '@babel/core': ^7.17.5 '@woocommerce/eslint-plugin': workspace:* '@wordpress/dependency-extraction-webpack-plugin': ^3.3.0 - eslint: ^8.12.0 + eslint: ^8.32.0 jest: ^27.5.1 jest-cli: ^27.5.1 rimraf: ^3.0.2 @@ -677,7 +677,7 @@ importers: devDependencies: '@babel/core': 7.17.8 '@woocommerce/eslint-plugin': link:../eslint-plugin - eslint: 8.12.0 + eslint: 8.32.0 jest: 27.5.1 jest-cli: 27.5.1 rimraf: 3.0.2 @@ -706,7 +706,7 @@ importers: '@wordpress/browserslist-config': ^4.1.0 '@wordpress/deprecated': ^3.2.3 config: 3.3.3 - eslint: ^8.12.0 + eslint: ^8.32.0 eslint-plugin-jest: 23.20.0 dependencies: '@jest/globals': 27.5.1 @@ -728,8 +728,8 @@ importers: '@wordpress/babel-plugin-import-jsx-pragma': 1.1.3_@babel+core@7.12.9 '@wordpress/babel-preset-default': 3.0.2_@babel+core@7.12.9 '@wordpress/browserslist-config': 4.1.0 - eslint: 8.12.0 - eslint-plugin-jest: 23.20.0_iqokrdhiz7bccawj5qurem2l4e + eslint: 8.32.0 + eslint-plugin-jest: 23.20.0_yygwinqv3a2io74xmwofqb7uka packages/js/e2e-environment: specifiers: @@ -824,7 +824,7 @@ importers: '@wordpress/deprecated': ^3.2.3 '@wordpress/e2e-test-utils': wp-5.8 config: 3.3.3 - eslint: ^8.1.0 + eslint: ^8.32.0 eslint-plugin-jest: 23.20.0 fishery: ^1.2.0 dependencies: @@ -843,22 +843,22 @@ importers: '@babel/plugin-transform-runtime': 7.16.4_@babel+core@7.12.9 '@babel/polyfill': 7.12.1 '@babel/preset-env': 7.12.7_@babel+core@7.12.9 - '@typescript-eslint/eslint-plugin': 5.43.0_77yfnzmbnonej4k3inkgdy4i5u - '@typescript-eslint/parser': 5.43.0_iqokrdhiz7bccawj5qurem2l4e + '@typescript-eslint/eslint-plugin': 5.43.0_7f3f6qw7x62gbtvc33ujooybme + '@typescript-eslint/parser': 5.43.0_yygwinqv3a2io74xmwofqb7uka '@woocommerce/eslint-plugin': link:../eslint-plugin '@woocommerce/internal-e2e-builds': link:../internal-e2e-builds '@wordpress/babel-plugin-import-jsx-pragma': 1.1.3_@babel+core@7.12.9 '@wordpress/babel-preset-default': 3.0.2_@babel+core@7.12.9 '@wordpress/browserslist-config': 4.1.0 - eslint: 8.12.0 - eslint-plugin-jest: 23.20.0_iqokrdhiz7bccawj5qurem2l4e + eslint: 8.32.0 + eslint-plugin-jest: 23.20.0_yygwinqv3a2io74xmwofqb7uka packages/js/eslint-plugin: specifiers: '@babel/core': ^7.17.5 '@typescript-eslint/parser': ^5.14.0 '@wordpress/eslint-plugin': ^13.3.0 - eslint: ^8.25.0 + eslint: ^8.32.0 eslint-plugin-react-hooks: ^4.3.0 eslint-plugin-testing-library: ^5.1.0 jest: ^27.5.1 @@ -868,14 +868,14 @@ importers: ts-jest: ^27.1.3 typescript: ^4.8.3 dependencies: - '@typescript-eslint/parser': 5.15.0_z4bbprzjrhnsfa24uvmcbu7f5q - '@wordpress/eslint-plugin': 13.3.0_fbyahr2bitmcepmkuzm2z5k2za - eslint-plugin-react-hooks: 4.3.0_eslint@8.25.0 - eslint-plugin-testing-library: 5.1.0_z4bbprzjrhnsfa24uvmcbu7f5q + '@typescript-eslint/parser': 5.15.0_yygwinqv3a2io74xmwofqb7uka + '@wordpress/eslint-plugin': 13.3.0_gfyzvuspv2bauiwltqdx5274hy + eslint-plugin-react-hooks: 4.3.0_eslint@8.32.0 + eslint-plugin-testing-library: 5.1.0_yygwinqv3a2io74xmwofqb7uka requireindex: 1.2.0 devDependencies: '@babel/core': 7.17.8 - eslint: 8.25.0 + eslint: 8.32.0 jest: 27.5.1 jest-cli: 27.5.1 rimraf: 3.0.2 @@ -910,7 +910,7 @@ importers: concurrently: ^7.0.0 css-loader: ^3.6.0 dompurify: ^2.3.6 - eslint: ^8.12.0 + eslint: ^8.32.0 gridicons: ^3.4.0 jest: ^27.5.1 jest-cli: ^27.5.1 @@ -946,7 +946,7 @@ importers: '@babel/runtime': 7.17.7 '@storybook/addon-actions': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/addon-console': 1.2.3_kthckm6zfmobggl2ahqbjihlce - '@storybook/react': 6.4.19_glozp6fblhaty2oacwbjl7ao2i + '@storybook/react': 6.4.19_pjugpuchrb7ea5kuxwnxihy6zq '@testing-library/dom': 8.11.3 '@testing-library/react': 12.1.4_sfoxds7t5ydpegc3knd667wn6m '@testing-library/user-event': 13.5.0_gzufz4q333be4gqfrvipwvqt6a @@ -960,7 +960,7 @@ importers: '@wordpress/browserslist-config': 4.1.2 concurrently: 7.0.0 css-loader: 3.6.0_webpack@5.70.0 - eslint: 8.12.0 + eslint: 8.32.0 jest: 27.5.1 jest-cli: 27.5.1 postcss-loader: 3.0.0 @@ -985,7 +985,7 @@ importers: '@wordpress/api-fetch': ^6.0.1 '@wordpress/hooks': ^3.5.0 cookie: ^0.4.2 - eslint: ^8.12.0 + eslint: ^8.32.0 jest: ^27.5.1 jest-cli: ^27.5.1 qs: ^6.10.3 @@ -1007,7 +1007,7 @@ importers: '@types/qs': 6.9.7 '@types/react': 17.0.50 '@woocommerce/eslint-plugin': link:../eslint-plugin - eslint: 8.12.0 + eslint: 8.32.0 jest: 27.5.1 jest-cli: 27.5.1 rimraf: 3.0.2 @@ -1022,7 +1022,7 @@ importers: '@babel/core': 7.12.9 '@woocommerce/eslint-plugin': workspace:* chalk: ^4.1.2 - eslint: ^8.12.0 + eslint: ^8.32.0 glob: ^7.2.0 lodash: ^4.17.21 mkdirp: ^1.0.4 @@ -1030,7 +1030,7 @@ importers: '@babel/core': 7.12.9 '@woocommerce/eslint-plugin': link:../eslint-plugin chalk: 4.1.2 - eslint: 8.12.0 + eslint: 8.32.0 glob: 7.2.0 lodash: 4.17.21 mkdirp: 1.0.4 @@ -1044,7 +1044,7 @@ importers: '@wordpress/data': ^6.15.0 '@wordpress/i18n': ^4.3.1 '@wordpress/jest-console': ^5.0.1 - eslint: ^8.12.0 + eslint: ^8.32.0 jest: ^27.5.1 jest-cli: ^27.5.1 regenerator-runtime: ^0.13.9 @@ -1061,7 +1061,7 @@ importers: devDependencies: '@babel/core': 7.17.8 '@woocommerce/eslint-plugin': link:../eslint-plugin - eslint: 8.12.0 + eslint: 8.32.0 jest: 27.5.1 jest-cli: 27.5.1 rimraf: 3.0.2 @@ -1076,7 +1076,7 @@ importers: '@wordpress/base-styles': ^4.3.0 '@wordpress/postcss-plugins-preset': ^1.6.0 css-loader: ^3.6.0 - eslint: ^8.12.0 + eslint: ^8.32.0 jest: ^27.5.1 jest-cli: ^27.5.1 mini-css-extract-plugin: ^2.6.0 @@ -1101,7 +1101,7 @@ importers: devDependencies: '@babel/core': 7.17.8 '@woocommerce/eslint-plugin': link:../eslint-plugin - eslint: 8.12.0 + eslint: 8.32.0 jest: 27.5.1 jest-cli: 27.5.1 rimraf: 3.0.2 @@ -1123,7 +1123,7 @@ importers: '@wordpress/hooks': ^3.5.0 '@wordpress/notices': ^3.3.2 '@wordpress/url': ^3.4.1 - eslint: ^8.12.0 + eslint: ^8.32.0 history: ^5.3.0 jest: ^27.5.1 jest-cli: ^27.5.1 @@ -1151,7 +1151,7 @@ importers: '@types/jest': 27.4.1 '@types/qs': 6.9.7 '@woocommerce/eslint-plugin': link:../eslint-plugin - eslint: 8.12.0 + eslint: 8.32.0 jest: 27.5.1 jest-cli: 27.5.1 rimraf: 3.0.2 @@ -1169,7 +1169,7 @@ importers: '@wordpress/a11y': ^3.5.0 '@wordpress/data': ^6.15.0 '@wordpress/notices': ^3.3.2 - eslint: ^8.12.0 + eslint: ^8.32.0 jest: ^27.5.1 jest-cli: ^27.5.1 lodash: ^4.17.0 @@ -1193,7 +1193,7 @@ importers: '@types/wordpress__data': 6.0.0 '@types/wordpress__notices': 3.5.0 '@woocommerce/eslint-plugin': link:../eslint-plugin - eslint: 8.12.0 + eslint: 8.32.0 jest: 27.5.1 jest-cli: 27.5.1 redux: 4.2.0 @@ -1208,7 +1208,7 @@ importers: '@types/jest': ^27.4.1 '@types/lodash': ^4.14.184 '@woocommerce/eslint-plugin': workspace:* - eslint: ^8.12.0 + eslint: ^8.32.0 jest: ^27.5.1 jest-cli: ^27.5.1 locutus: ^2.0.16 @@ -1223,7 +1223,7 @@ importers: '@types/jest': 27.4.1 '@types/lodash': 4.14.184 '@woocommerce/eslint-plugin': link:../eslint-plugin - eslint: 8.12.0 + eslint: 8.32.0 jest: 27.5.1 jest-cli: 27.5.1 rimraf: 3.0.2 @@ -1248,7 +1248,7 @@ importers: '@wordpress/i18n': ^4.3.1 concurrently: ^7.0.0 css-loader: ^3.6.0 - eslint: ^8.12.0 + eslint: ^8.32.0 gridicons: ^3.4.0 jest: ^27.5.1 jest-cli: ^27.5.1 @@ -1278,7 +1278,7 @@ importers: '@woocommerce/internal-style-build': link:../internal-style-build '@wordpress/browserslist-config': 4.1.2 css-loader: 3.6.0_webpack@5.70.0 - eslint: 8.12.0 + eslint: 8.32.0 jest: 27.5.1 jest-cli: 27.5.1 postcss-loader: 3.0.0 @@ -1299,7 +1299,7 @@ importers: '@wordpress/components': ^19.5.0 '@wordpress/element': ^4.1.1 css-loader: ^3.6.0 - eslint: ^8.12.0 + eslint: ^8.32.0 jest: ^27.5.1 jest-cli: ^27.5.1 postcss-loader: ^3.0.0 @@ -1318,7 +1318,7 @@ importers: '@woocommerce/internal-style-build': link:../internal-style-build '@wordpress/browserslist-config': 4.1.3 css-loader: 3.6.0_webpack@5.70.0 - eslint: 8.28.0 + eslint: 8.32.0 jest: 27.5.1 jest-cli: 27.5.1 postcss-loader: 3.0.0 @@ -1334,7 +1334,7 @@ importers: '@types/debug': ^4.1.7 '@woocommerce/eslint-plugin': workspace:* debug: ^4.3.3 - eslint: ^8.12.0 + eslint: ^8.32.0 jest: ^27.5.1 jest-cli: ^27.5.1 rimraf: ^3.0.2 @@ -1346,7 +1346,7 @@ importers: '@babel/core': 7.17.8 '@types/debug': 4.1.7 '@woocommerce/eslint-plugin': link:../eslint-plugin - eslint: 8.12.0 + eslint: 8.32.0 jest: 27.5.1 jest-cli: 27.5.1 rimraf: 3.0.2 @@ -1387,7 +1387,7 @@ importers: cross-env: 6.0.3 deasync: 0.1.26 dotenv: ^10.0.0 - eslint: ^8.12.0 + eslint: ^8.32.0 eslint-config-wpcalypso: 5.0.0 eslint-plugin-jest: 23.20.0 istanbul: 1.0.0-alpha.2 @@ -1408,9 +1408,9 @@ importers: '@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 - '@typescript-eslint/eslint-plugin': 5.43.0_hhpcbb6wqnhvo6wpcctutdxelq - '@typescript-eslint/experimental-utils': 5.43.0_z4bbprzjrhnsfa24uvmcbu7f5q - '@typescript-eslint/parser': 5.43.0_z4bbprzjrhnsfa24uvmcbu7f5q + '@typescript-eslint/eslint-plugin': 5.43.0_7f3f6qw7x62gbtvc33ujooybme + '@typescript-eslint/experimental-utils': 5.43.0_yygwinqv3a2io74xmwofqb7uka + '@typescript-eslint/parser': 5.43.0_yygwinqv3a2io74xmwofqb7uka '@woocommerce/admin-e2e-tests': link:../../packages/js/admin-e2e-tests '@woocommerce/api': link:../../packages/js/api '@woocommerce/api-core-tests': link:../../packages/js/api-core-tests @@ -1427,16 +1427,16 @@ importers: allure-playwright: 2.0.0-beta.19 autoprefixer: 9.8.6 axios: 0.24.0 - babel-eslint: 10.1.0_eslint@8.25.0 + 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 cross-env: 6.0.3 deasync: 0.1.26 dotenv: 10.0.0 - eslint: 8.25.0 - eslint-config-wpcalypso: 5.0.0_44ie4thsizlsiqkjwba6wctao4 - eslint-plugin-jest: 23.20.0_z4bbprzjrhnsfa24uvmcbu7f5q + eslint: 8.32.0 + eslint-config-wpcalypso: 5.0.0_3reg6c535tepz5eatsxybh5pja + eslint-plugin-jest: 23.20.0_yygwinqv3a2io74xmwofqb7uka istanbul: 1.0.0-alpha.2 jest: 27.5.1 mocha: 7.2.0 @@ -1567,7 +1567,7 @@ importers: css-loader: ^6.7.0 debug: ^4.3.3 dompurify: ^2.3.6 - eslint: ^8.10.0 + eslint: ^8.32.0 eslint-import-resolver-typescript: ^2.5.0 eslint-import-resolver-webpack: ^0.13.2 eslint-plugin-import: ^2.25.4 @@ -1710,8 +1710,8 @@ importers: '@types/wordpress__media-utils': 3.0.0_sfoxds7t5ydpegc3knd667wn6m '@types/wordpress__notices': 3.3.0 '@types/wordpress__plugins': 3.0.0_sfoxds7t5ydpegc3knd667wn6m - '@typescript-eslint/eslint-plugin': 5.43.0_xi2bidqtjw6phi5qq54cbyxqtu - '@typescript-eslint/parser': 5.43.0_himlt4eddny2rsb5zkuydvuf7u + '@typescript-eslint/eslint-plugin': 5.43.0_7f3f6qw7x62gbtvc33ujooybme + '@typescript-eslint/parser': 5.43.0_yygwinqv3a2io74xmwofqb7uka '@woocommerce/admin-e2e-tests': link:../../packages/js/admin-e2e-tests '@woocommerce/components': link:../../packages/js/components '@woocommerce/csv-export': link:../../packages/js/csv-export @@ -1754,13 +1754,13 @@ importers: copy-webpack-plugin: 10.2.4_webpack@5.70.0 cross-env: 7.0.3 css-loader: 6.7.1_webpack@5.70.0 - eslint: 8.11.0 - eslint-import-resolver-typescript: 2.5.0_7yrnqyx753fo5bwjhiag2wpedy + eslint: 8.32.0 + eslint-import-resolver-typescript: 2.5.0_xygrkdz7akfjaillbmnvbogh5e eslint-import-resolver-webpack: 0.13.2_xlbwhpbmf5dkmuyaaidudnwply - eslint-plugin-import: 2.25.4_mwel7v7qi7wwnb6yoytqwvyg4e - eslint-plugin-react: 7.29.4_eslint@8.11.0 + eslint-plugin-import: 2.25.4_wodm5nq5wrndbbcvxidaevzgna + eslint-plugin-react: 7.29.4_eslint@8.32.0 expose-loader: 3.1.0_webpack@5.70.0 - fork-ts-checker-webpack-plugin: 6.5.0_k7hdfuiil3x6lfvxi3pndxxjmi + fork-ts-checker-webpack-plugin: 6.5.0_5spy6wmzwqixc3k64gvf5wblh4 jest: 27.5.1 jest-environment-jsdom: 27.5.1 jest-environment-node: 27.5.1 @@ -1818,7 +1818,7 @@ importers: '@wordpress/hooks': ^2.11.1 '@wordpress/prettier-config': ^2.5.0 '@wordpress/scripts': ^19.2.4 - eslint: 5.16.0 + eslint: ^8.32.0 prettier: npm:wp-prettier@^2.6.2 prop-types: ^15.8.1 react: ^17.0.2 @@ -1849,8 +1849,8 @@ importers: '@woocommerce/eslint-plugin': link:../../packages/js/eslint-plugin '@wordpress/env': 4.9.0 '@wordpress/prettier-config': 2.5.0_wp-prettier@2.6.2 - '@wordpress/scripts': 19.2.4_acp5qrdj4cc6vmqqhp4stdanfe - eslint: 5.16.0 + '@wordpress/scripts': 19.2.4_f7x7zdz3ccrnqxb4utvdtwqz4e + eslint: 8.32.0 prettier: /wp-prettier/2.6.2 ts-loader: 9.4.1_27qmdvvfdw5s3nqwnln6yerdsa typescript: 4.8.4 @@ -1936,7 +1936,7 @@ importers: cli-core: workspace:* commander: ^9.4.0 dotenv: ^10.0.0 - eslint: ^8.12.0 + eslint: ^8.32.0 simple-git: ^3.10.0 ts-node: ^10.2.1 tslib: ^2.3.1 @@ -1955,7 +1955,7 @@ importers: devDependencies: '@types/node': 16.10.3 '@woocommerce/eslint-plugin': link:../../packages/js/eslint-plugin - eslint: 8.25.0 + eslint: 8.32.0 ts-node: 10.9.1_66qcjwcvmucahiv4aiph345ggy tslib: 2.3.1 typescript: 4.8.4 @@ -1979,7 +1979,7 @@ importers: '@octokit/request-error': ^3.0.1 '@types/node': ^16.9.4 '@woocommerce/eslint-plugin': workspace:* - eslint: ^8.12.0 + eslint: ^8.32.0 globby: ^11 jscodeshift: ^0.13.1 oclif: ^2 @@ -1996,7 +1996,7 @@ importers: '@octokit/request-error': 3.0.1 '@types/node': 16.10.3 '@woocommerce/eslint-plugin': link:../../packages/js/eslint-plugin - eslint: 8.25.0 + eslint: 8.32.0 globby: 11.0.4 jscodeshift: 0.13.1_@babel+preset-env@7.19.3 oclif: 2.7.0_7yoz4vugw4qcykie6sit5r22dm @@ -2591,7 +2591,7 @@ packages: transitivePeerDependencies: - supports-color - /@babel/eslint-parser/7.17.0_2sahtlaqcjxbvw6x6d2jsqyrai: + /@babel/eslint-parser/7.17.0_45t77ya3ofqya5ogk4q6xdzcpq: resolution: {integrity: sha512-PUEJ7ZBXbRkbq3qqM/jZ2nIuakUBqCYc7Qf52Lj7dlZ6zERnqisdHioL0l4wwQZnmskMeasqUNzLBFKs3nylXA==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} peerDependencies: @@ -2599,13 +2599,13 @@ packages: eslint: ^7.5.0 || ^8.0.0 dependencies: '@babel/core': 7.17.8 - eslint: 8.25.0 + eslint: 8.32.0 eslint-scope: 5.1.1 eslint-visitor-keys: 2.1.0 semver: 6.3.0 dev: false - /@babel/eslint-parser/7.17.0_egbv6vphnwurm3ap2o4feqemum: + /@babel/eslint-parser/7.17.0_ghetaprrho5qxr5lt7srlprvom: resolution: {integrity: sha512-PUEJ7ZBXbRkbq3qqM/jZ2nIuakUBqCYc7Qf52Lj7dlZ6zERnqisdHioL0l4wwQZnmskMeasqUNzLBFKs3nylXA==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} peerDependencies: @@ -2613,7 +2613,7 @@ packages: eslint: ^7.5.0 || ^8.0.0 dependencies: '@babel/core': 7.12.9 - eslint: 8.28.0 + eslint: 8.32.0 eslint-scope: 5.1.1 eslint-visitor-keys: 2.1.0 semver: 6.3.0 @@ -2687,32 +2687,6 @@ packages: semver: 6.3.0 dev: true - /@babel/helper-compilation-targets/7.17.7_@babel+core@7.12.9: - resolution: {integrity: sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/compat-data': 7.19.3 - '@babel/core': 7.12.9 - '@babel/helper-validator-option': 7.18.6 - browserslist: 4.21.4 - semver: 6.3.0 - dev: true - - /@babel/helper-compilation-targets/7.17.7_@babel+core@7.16.12: - resolution: {integrity: sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/compat-data': 7.19.3 - '@babel/core': 7.16.12 - '@babel/helper-validator-option': 7.18.6 - browserslist: 4.21.4 - semver: 6.3.0 - dev: false - /@babel/helper-compilation-targets/7.17.7_@babel+core@7.17.8: resolution: {integrity: sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==} engines: {node: '>=6.9.0'} @@ -4165,7 +4139,7 @@ packages: '@babel/core': 7.17.8 '@babel/helper-annotate-as-pure': 7.16.7 '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.17.8 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.17.8 transitivePeerDependencies: - supports-color @@ -6375,9 +6349,9 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-module-imports': 7.16.0 - '@babel/helper-plugin-utils': 7.14.5 - babel-plugin-polyfill-corejs2: 0.3.0_@babel+core@7.16.12 + '@babel/helper-module-imports': 7.18.6 + '@babel/helper-plugin-utils': 7.19.0 + babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.16.12 babel-plugin-polyfill-corejs3: 0.4.0_@babel+core@7.16.12 babel-plugin-polyfill-regenerator: 0.3.0_@babel+core@7.16.12 semver: 6.3.0 @@ -6392,9 +6366,9 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-module-imports': 7.16.0 - '@babel/helper-plugin-utils': 7.14.5 - babel-plugin-polyfill-corejs2: 0.3.0_@babel+core@7.17.8 + '@babel/helper-module-imports': 7.18.6 + '@babel/helper-plugin-utils': 7.19.0 + babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.17.8 babel-plugin-polyfill-corejs3: 0.4.0_@babel+core@7.17.8 babel-plugin-polyfill-regenerator: 0.3.0_@babel+core@7.17.8 semver: 6.3.0 @@ -6903,7 +6877,7 @@ packages: dependencies: '@babel/compat-data': 7.17.7 '@babel/core': 7.12.9 - '@babel/helper-compilation-targets': 7.17.7_@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 @@ -6970,7 +6944,7 @@ packages: '@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/preset-modules': 0.1.5_@babel+core@7.12.9 - '@babel/types': 7.17.0 + '@babel/types': 7.19.3 babel-plugin-polyfill-corejs2: 0.3.0_@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 @@ -6988,7 +6962,7 @@ packages: dependencies: '@babel/compat-data': 7.17.7 '@babel/core': 7.16.12 - '@babel/helper-compilation-targets': 7.17.7_@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 @@ -7055,7 +7029,7 @@ packages: '@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/preset-modules': 0.1.5_@babel+core@7.16.12 - '@babel/types': 7.17.0 + '@babel/types': 7.19.3 babel-plugin-polyfill-corejs2: 0.3.0_@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 @@ -7306,8 +7280,8 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 - '@babel/helper-validator-option': 7.16.7 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-validator-option': 7.18.6 '@babel/plugin-transform-typescript': 7.16.8_@babel+core@7.16.12 transitivePeerDependencies: - supports-color @@ -7834,7 +7808,7 @@ packages: ajv: 6.12.6 debug: 4.3.4 espree: 7.3.1 - globals: 13.18.0 + globals: 13.19.0 ignore: 4.0.6 import-fresh: 3.3.0 js-yaml: 3.14.1 @@ -7844,48 +7818,14 @@ packages: - supports-color dev: true - /@eslint/eslintrc/1.0.4: - resolution: {integrity: sha512-h8Vx6MdxwWI2WM8/zREHMoqdgLNXEL4QX3MWSVMdyNJGvXVOs+6lp+m2hc3FnuMHDc4poxFNI20vCk0OmI4G0Q==} + /@eslint/eslintrc/1.4.1: + resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 debug: 4.3.4 espree: 9.4.1 - globals: 13.18.0 - ignore: 4.0.6 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@eslint/eslintrc/1.2.1: - resolution: {integrity: sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - ajv: 6.12.6 - debug: 4.3.4 - espree: 9.4.1 - globals: 13.18.0 - ignore: 5.2.0 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@eslint/eslintrc/1.3.3: - resolution: {integrity: sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - ajv: 6.12.6 - debug: 4.3.4 - espree: 9.4.1 - globals: 13.18.0 + globals: 13.19.0 ignore: 5.2.0 import-fresh: 3.3.0 js-yaml: 4.1.0 @@ -8002,18 +7942,8 @@ packages: dependencies: '@hapi/hoek': 9.3.0 - /@humanwhocodes/config-array/0.10.7: - resolution: {integrity: sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==} - engines: {node: '>=10.10.0'} - dependencies: - '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - /@humanwhocodes/config-array/0.11.7: - resolution: {integrity: sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==} + /@humanwhocodes/config-array/0.11.8: + resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==} engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 1.2.1 @@ -8033,28 +7963,6 @@ packages: - supports-color dev: true - /@humanwhocodes/config-array/0.6.0: - resolution: {integrity: sha512-JQlEKbcgEUjBFhLIF4iqM7u/9lwgHRBcpHrmUNCALK0Q3amXN6lxdoXLnF0sm11E9VqTmBALR87IlUg1bZ8A9A==} - engines: {node: '>=10.10.0'} - dependencies: - '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@humanwhocodes/config-array/0.9.5: - resolution: {integrity: sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==} - engines: {node: '>=10.10.0'} - dependencies: - '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - dev: true - /@humanwhocodes/module-importer/1.0.1: resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} @@ -10024,7 +9932,7 @@ packages: global: 4.4.0 dev: true - /@storybook/addon-controls/6.4.19_3mpzmuykh5ctyyi3r2d2agoucu: + /@storybook/addon-controls/6.4.19_56jbash75ng5psbctf36wqywr4: resolution: {integrity: sha512-JHi5z9i6NsgQLfG5WOeQE1AyOrM+QJLrjT+uOYx40bq+OC1yWHH7qHiphPP8kjJJhCZlaQk1qqXYkkQXgaeHSw==} peerDependencies: react: ^16.8.0 || ^17.0.0 @@ -10038,8 +9946,8 @@ packages: '@storybook/addons': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/api': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/client-logger': 6.4.19 - '@storybook/components': 6.4.19_hiunvzosbwliizyirxfy6hjyim - '@storybook/core-common': 6.4.19_bhvadzvbuq4c4gucumdoppg3by + '@storybook/components': 6.4.19_sfoxds7t5ydpegc3knd667wn6m + '@storybook/core-common': 6.4.19_56jbash75ng5psbctf36wqywr4 '@storybook/csf': 0.0.2--canary.87bc651.0 '@storybook/node-logger': 6.4.19 '@storybook/store': 6.4.19_sfoxds7t5ydpegc3knd667wn6m @@ -10059,7 +9967,7 @@ packages: - webpack-command dev: true - /@storybook/addon-controls/6.4.19_56jbash75ng5psbctf36wqywr4: + /@storybook/addon-controls/6.4.19_wl7ffu5ts6ayqm24qlkw7h6j4e: resolution: {integrity: sha512-JHi5z9i6NsgQLfG5WOeQE1AyOrM+QJLrjT+uOYx40bq+OC1yWHH7qHiphPP8kjJJhCZlaQk1qqXYkkQXgaeHSw==} peerDependencies: react: ^16.8.0 || ^17.0.0 @@ -10073,8 +9981,8 @@ packages: '@storybook/addons': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/api': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/client-logger': 6.4.19 - '@storybook/components': 6.4.19_sfoxds7t5ydpegc3knd667wn6m - '@storybook/core-common': 6.4.19_56jbash75ng5psbctf36wqywr4 + '@storybook/components': 6.4.19_hiunvzosbwliizyirxfy6hjyim + '@storybook/core-common': 6.4.19_mqzgkamhc7bbbitv65cxtf4gfa '@storybook/csf': 0.0.2--canary.87bc651.0 '@storybook/node-logger': 6.4.19 '@storybook/store': 6.4.19_sfoxds7t5ydpegc3knd667wn6m @@ -10207,7 +10115,7 @@ packages: - webpack-command dev: true - /@storybook/addon-docs/6.4.19_fzsnscfffpgd4jw4kgkdqz7wca: + /@storybook/addon-docs/6.4.19_td5ldnqdrdzplltfsjeiin6b2q: resolution: {integrity: sha512-OEPyx/5ZXmZOPqIAWoPjlIP8Q/YfNjAmBosA8tmA8t5KCSiq/vpLcAvQhxqK6n0wk/B8Xp67Z8RpLfXjU8R3tw==} peerDependencies: '@storybook/angular': 6.4.19 @@ -10265,17 +10173,17 @@ packages: '@mdx-js/react': 1.6.22_react@17.0.2 '@storybook/addons': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/api': 6.4.19_sfoxds7t5ydpegc3knd667wn6m - '@storybook/builder-webpack4': 6.4.19_vrzxlgakqf7u5t766r72d2xf3u + '@storybook/builder-webpack4': 6.4.19_yuv5drn7ijijfjkwa6mxoauvrq '@storybook/client-logger': 6.4.19 '@storybook/components': 6.4.19_hiunvzosbwliizyirxfy6hjyim - '@storybook/core': 6.4.19_uog2ckjumoqoyqssiweskrah3a + '@storybook/core': 6.4.19_6cqelbxt6lx74wa4u7ecuuny7i '@storybook/core-events': 6.4.19 '@storybook/csf': 0.0.2--canary.87bc651.0 '@storybook/csf-tools': 6.4.19 '@storybook/node-logger': 6.4.19 '@storybook/postinstall': 6.4.19 '@storybook/preview-web': 6.4.19_sfoxds7t5ydpegc3knd667wn6m - '@storybook/react': 6.4.19_oycjqkyefi4akx2twppuux3udq + '@storybook/react': 6.4.19_cqdgeqmmrux6joug3kc73q4l6m '@storybook/source-loader': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/store': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/theming': 6.4.19_sfoxds7t5ydpegc3knd667wn6m @@ -10491,6 +10399,99 @@ packages: util-deprecate: 1.0.2 dev: true + /@storybook/builder-webpack4/6.4.19_25m6hjymtvcmw3ioyxirlpsioi: + resolution: {integrity: sha512-wxA6SMH11duc9D53aeVVBwrVRemFIoxHp/dOugkkg6ZZFAb4ZmWzf/ENc3vQIZdZpfNRi7IZIZEOfoHc994cmw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@babel/core': 7.17.8 + '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.17.8 + '@babel/plugin-proposal-decorators': 7.16.4_@babel+core@7.17.8 + '@babel/plugin-proposal-export-default-from': 7.16.7_@babel+core@7.17.8 + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.17.8 + '@babel/plugin-proposal-object-rest-spread': 7.18.9_@babel+core@7.17.8 + '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.17.8 + '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.17.8 + '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.17.8 + '@babel/plugin-transform-arrow-functions': 7.18.6_@babel+core@7.17.8 + '@babel/plugin-transform-block-scoping': 7.18.9_@babel+core@7.17.8 + '@babel/plugin-transform-classes': 7.19.0_@babel+core@7.17.8 + '@babel/plugin-transform-destructuring': 7.18.13_@babel+core@7.17.8 + '@babel/plugin-transform-for-of': 7.18.8_@babel+core@7.17.8 + '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.17.8 + '@babel/plugin-transform-shorthand-properties': 7.18.6_@babel+core@7.17.8 + '@babel/plugin-transform-spread': 7.19.0_@babel+core@7.17.8 + '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.17.8 + '@babel/preset-env': 7.19.3_@babel+core@7.17.8 + '@babel/preset-react': 7.16.7_@babel+core@7.17.8 + '@babel/preset-typescript': 7.18.6_@babel+core@7.17.8 + '@storybook/addons': 6.4.19_sfoxds7t5ydpegc3knd667wn6m + '@storybook/api': 6.4.19_sfoxds7t5ydpegc3knd667wn6m + '@storybook/channel-postmessage': 6.4.19 + '@storybook/channels': 6.4.19 + '@storybook/client-api': 6.4.19_sfoxds7t5ydpegc3knd667wn6m + '@storybook/client-logger': 6.4.19 + '@storybook/components': 6.4.19_hiunvzosbwliizyirxfy6hjyim + '@storybook/core-common': 6.4.19_mqzgkamhc7bbbitv65cxtf4gfa + '@storybook/core-events': 6.4.19 + '@storybook/node-logger': 6.4.19 + '@storybook/preview-web': 6.4.19_sfoxds7t5ydpegc3knd667wn6m + '@storybook/router': 6.4.19_sfoxds7t5ydpegc3knd667wn6m + '@storybook/semver': 7.3.2 + '@storybook/store': 6.4.19_sfoxds7t5ydpegc3knd667wn6m + '@storybook/theming': 6.4.19_sfoxds7t5ydpegc3knd667wn6m + '@storybook/ui': 6.4.19_hiunvzosbwliizyirxfy6hjyim + '@types/node': 14.14.33 + '@types/webpack': 4.41.32 + autoprefixer: 9.8.6 + babel-loader: 8.2.3_w4x3pzrj2omidyjy5w3nzug7xy + babel-plugin-macros: 2.8.0 + babel-plugin-polyfill-corejs3: 0.1.7_@babel+core@7.17.8 + case-sensitive-paths-webpack-plugin: 2.4.0 + core-js: 3.25.5 + css-loader: 3.6.0_webpack@4.46.0 + file-loader: 6.2.0_webpack@4.46.0 + find-up: 5.0.0 + fork-ts-checker-webpack-plugin: 4.1.6_62fz4gragfs3w5a4iegi53ru5i + glob: 7.2.0 + glob-promise: 3.4.0_glob@7.2.0 + global: 4.4.0 + html-webpack-plugin: 4.5.2_webpack@4.46.0 + pnp-webpack-plugin: 1.6.4_typescript@4.8.4 + postcss: 7.0.39 + postcss-flexbugs-fixes: 4.2.1 + postcss-loader: 4.2.0_gzaxsinx64nntyd3vmdqwl7coe + raw-loader: 4.0.2_webpack@4.46.0 + react: 17.0.2 + react-dom: 17.0.2_react@17.0.2 + stable: 0.1.8 + style-loader: 1.3.0_webpack@4.46.0 + terser-webpack-plugin: 4.2.3_acorn@8.8.1+webpack@4.46.0 + ts-dedent: 2.2.0 + typescript: 4.8.4 + url-loader: 4.1.1_lit45vopotvaqup7lrvlnvtxwy + util-deprecate: 1.0.2 + webpack: 4.46.0_webpack-cli@3.3.12 + webpack-dev-middleware: 3.7.3_webpack@4.46.0 + webpack-filter-warnings-plugin: 1.2.1_webpack@4.46.0 + webpack-hot-middleware: 2.25.1 + webpack-virtual-modules: 0.2.2 + transitivePeerDependencies: + - '@types/react' + - acorn + - bluebird + - eslint + - supports-color + - vue-template-compiler + - webpack-cli + - webpack-command + dev: true + /@storybook/builder-webpack4/6.4.19_3n4gsnmxucj3bywv6syggoiztm: resolution: {integrity: sha512-wxA6SMH11duc9D53aeVVBwrVRemFIoxHp/dOugkkg6ZZFAb4ZmWzf/ENc3vQIZdZpfNRi7IZIZEOfoHc994cmw==} peerDependencies: @@ -10677,7 +10678,7 @@ packages: - webpack-command dev: true - /@storybook/builder-webpack4/6.4.19_fukhgakaronpnxxxb7r2advxsa: + /@storybook/builder-webpack4/6.4.19_o7sd63cu7bkdykipq4mkrzuw2i: resolution: {integrity: sha512-wxA6SMH11duc9D53aeVVBwrVRemFIoxHp/dOugkkg6ZZFAb4ZmWzf/ENc3vQIZdZpfNRi7IZIZEOfoHc994cmw==} peerDependencies: react: ^16.8.0 || ^17.0.0 @@ -10715,7 +10716,7 @@ packages: '@storybook/client-api': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/client-logger': 6.4.19 '@storybook/components': 6.4.19_sfoxds7t5ydpegc3knd667wn6m - '@storybook/core-common': 6.4.19_bhvadzvbuq4c4gucumdoppg3by + '@storybook/core-common': 6.4.19_mqzgkamhc7bbbitv65cxtf4gfa '@storybook/core-events': 6.4.19 '@storybook/node-logger': 6.4.19 '@storybook/preview-web': 6.4.19_sfoxds7t5ydpegc3knd667wn6m @@ -10735,7 +10736,7 @@ packages: css-loader: 3.6.0_webpack@4.46.0 file-loader: 6.2.0_webpack@4.46.0 find-up: 5.0.0 - fork-ts-checker-webpack-plugin: 4.1.6_hsuy55rzeh5wgjgxknlp6ffswa + fork-ts-checker-webpack-plugin: 4.1.6_62fz4gragfs3w5a4iegi53ru5i glob: 7.2.0 glob-promise: 3.4.0_glob@7.2.0 global: 4.4.0 @@ -10770,7 +10771,7 @@ packages: - webpack-command dev: true - /@storybook/builder-webpack4/6.4.19_kdglyhz445ek5yhe73f5yegd3m: + /@storybook/builder-webpack4/6.4.19_yuv5drn7ijijfjkwa6mxoauvrq: resolution: {integrity: sha512-wxA6SMH11duc9D53aeVVBwrVRemFIoxHp/dOugkkg6ZZFAb4ZmWzf/ENc3vQIZdZpfNRi7IZIZEOfoHc994cmw==} peerDependencies: react: ^16.8.0 || ^17.0.0 @@ -10808,7 +10809,7 @@ packages: '@storybook/client-api': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/client-logger': 6.4.19 '@storybook/components': 6.4.19_hiunvzosbwliizyirxfy6hjyim - '@storybook/core-common': 6.4.19_bhvadzvbuq4c4gucumdoppg3by + '@storybook/core-common': 6.4.19_mqzgkamhc7bbbitv65cxtf4gfa '@storybook/core-events': 6.4.19 '@storybook/node-logger': 6.4.19 '@storybook/preview-web': 6.4.19_sfoxds7t5ydpegc3knd667wn6m @@ -10828,100 +10829,7 @@ packages: css-loader: 3.6.0_webpack@4.46.0 file-loader: 6.2.0_webpack@4.46.0 find-up: 5.0.0 - fork-ts-checker-webpack-plugin: 4.1.6_hsuy55rzeh5wgjgxknlp6ffswa - glob: 7.2.0 - glob-promise: 3.4.0_glob@7.2.0 - global: 4.4.0 - html-webpack-plugin: 4.5.2_webpack@4.46.0 - pnp-webpack-plugin: 1.6.4_typescript@4.8.4 - postcss: 7.0.39 - postcss-flexbugs-fixes: 4.2.1 - postcss-loader: 4.2.0_gzaxsinx64nntyd3vmdqwl7coe - raw-loader: 4.0.2_webpack@4.46.0 - react: 17.0.2 - react-dom: 17.0.2_react@17.0.2 - stable: 0.1.8 - style-loader: 1.3.0_webpack@4.46.0 - terser-webpack-plugin: 4.2.3_acorn@8.8.1+webpack@4.46.0 - ts-dedent: 2.2.0 - typescript: 4.8.4 - url-loader: 4.1.1_lit45vopotvaqup7lrvlnvtxwy - util-deprecate: 1.0.2 - webpack: 4.46.0_webpack-cli@3.3.12 - webpack-dev-middleware: 3.7.3_webpack@4.46.0 - webpack-filter-warnings-plugin: 1.2.1_webpack@4.46.0 - webpack-hot-middleware: 2.25.1 - webpack-virtual-modules: 0.2.2 - transitivePeerDependencies: - - '@types/react' - - acorn - - bluebird - - eslint - - supports-color - - vue-template-compiler - - webpack-cli - - webpack-command - dev: true - - /@storybook/builder-webpack4/6.4.19_vrzxlgakqf7u5t766r72d2xf3u: - resolution: {integrity: sha512-wxA6SMH11duc9D53aeVVBwrVRemFIoxHp/dOugkkg6ZZFAb4ZmWzf/ENc3vQIZdZpfNRi7IZIZEOfoHc994cmw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 - react-dom: ^16.8.0 || ^17.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@babel/core': 7.17.8 - '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.17.8 - '@babel/plugin-proposal-decorators': 7.16.4_@babel+core@7.17.8 - '@babel/plugin-proposal-export-default-from': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.17.8 - '@babel/plugin-proposal-object-rest-spread': 7.18.9_@babel+core@7.17.8 - '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.17.8 - '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.17.8 - '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.17.8 - '@babel/plugin-transform-arrow-functions': 7.18.6_@babel+core@7.17.8 - '@babel/plugin-transform-block-scoping': 7.18.9_@babel+core@7.17.8 - '@babel/plugin-transform-classes': 7.19.0_@babel+core@7.17.8 - '@babel/plugin-transform-destructuring': 7.18.13_@babel+core@7.17.8 - '@babel/plugin-transform-for-of': 7.18.8_@babel+core@7.17.8 - '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.17.8 - '@babel/plugin-transform-shorthand-properties': 7.18.6_@babel+core@7.17.8 - '@babel/plugin-transform-spread': 7.19.0_@babel+core@7.17.8 - '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.17.8 - '@babel/preset-env': 7.19.3_@babel+core@7.17.8 - '@babel/preset-react': 7.16.7_@babel+core@7.17.8 - '@babel/preset-typescript': 7.18.6_@babel+core@7.17.8 - '@storybook/addons': 6.4.19_sfoxds7t5ydpegc3knd667wn6m - '@storybook/api': 6.4.19_sfoxds7t5ydpegc3knd667wn6m - '@storybook/channel-postmessage': 6.4.19 - '@storybook/channels': 6.4.19 - '@storybook/client-api': 6.4.19_sfoxds7t5ydpegc3knd667wn6m - '@storybook/client-logger': 6.4.19 - '@storybook/components': 6.4.19_hiunvzosbwliizyirxfy6hjyim - '@storybook/core-common': 6.4.19_bhvadzvbuq4c4gucumdoppg3by - '@storybook/core-events': 6.4.19 - '@storybook/node-logger': 6.4.19 - '@storybook/preview-web': 6.4.19_sfoxds7t5ydpegc3knd667wn6m - '@storybook/router': 6.4.19_sfoxds7t5ydpegc3knd667wn6m - '@storybook/semver': 7.3.2 - '@storybook/store': 6.4.19_sfoxds7t5ydpegc3knd667wn6m - '@storybook/theming': 6.4.19_sfoxds7t5ydpegc3knd667wn6m - '@storybook/ui': 6.4.19_hiunvzosbwliizyirxfy6hjyim - '@types/node': 14.14.33 - '@types/webpack': 4.41.32 - autoprefixer: 9.8.6 - babel-loader: 8.2.3_w4x3pzrj2omidyjy5w3nzug7xy - babel-plugin-macros: 2.8.0 - babel-plugin-polyfill-corejs3: 0.1.7_@babel+core@7.17.8 - case-sensitive-paths-webpack-plugin: 2.4.0 - core-js: 3.25.5 - css-loader: 3.6.0_webpack@4.46.0 - file-loader: 6.2.0_webpack@4.46.0 - find-up: 5.0.0 - fork-ts-checker-webpack-plugin: 4.1.6_hsuy55rzeh5wgjgxknlp6ffswa + fork-ts-checker-webpack-plugin: 4.1.6_62fz4gragfs3w5a4iegi53ru5i glob: 7.2.0 glob-promise: 3.4.0_glob@7.2.0 global: 4.4.0 @@ -11404,7 +11312,7 @@ packages: - webpack-command dev: true - /@storybook/core-common/6.4.19_bhvadzvbuq4c4gucumdoppg3by: + /@storybook/core-common/6.4.19_mqzgkamhc7bbbitv65cxtf4gfa: resolution: {integrity: sha512-X1pJJkO48DFxl6iyEemIKqRkJ7j9/cBh3BRBUr+xZHXBvnD0GKDXIocwh0PjSxSC6XSu3UCQnqtKi3PbjRl8Dg==} peerDependencies: react: ^16.8.0 || ^17.0.0 @@ -11447,7 +11355,7 @@ packages: express: 4.18.1 file-system-cache: 1.0.5 find-up: 5.0.0 - fork-ts-checker-webpack-plugin: 6.5.0_hsuy55rzeh5wgjgxknlp6ffswa + fork-ts-checker-webpack-plugin: 6.5.0_62fz4gragfs3w5a4iegi53ru5i fs-extra: 9.1.0 glob: 7.2.0 handlebars: 4.7.7 @@ -11480,6 +11388,81 @@ packages: core-js: 3.21.1 dev: true + /@storybook/core-server/6.4.19_25m6hjymtvcmw3ioyxirlpsioi: + resolution: {integrity: sha512-bKsUB9f7hl5ya2JXxpIrErmbDQjoH39FVbzYZWjMo4t/b7+Xyi6vYadwyWcqlpUQmis09ZaSMv8L/Tw0TuwLAA==} + peerDependencies: + '@storybook/builder-webpack5': 6.4.19 + '@storybook/manager-webpack5': 6.4.19 + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + typescript: '*' + peerDependenciesMeta: + '@storybook/builder-webpack5': + optional: true + '@storybook/manager-webpack5': + optional: true + typescript: + optional: true + dependencies: + '@discoveryjs/json-ext': 0.5.7 + '@storybook/builder-webpack4': 6.4.19_25m6hjymtvcmw3ioyxirlpsioi + '@storybook/core-client': 6.4.19_4khy3msxr4lnrhwh6cbg2lwt64 + '@storybook/core-common': 6.4.19_mqzgkamhc7bbbitv65cxtf4gfa + '@storybook/core-events': 6.4.19 + '@storybook/csf': 0.0.2--canary.87bc651.0 + '@storybook/csf-tools': 6.4.19 + '@storybook/manager-webpack4': 6.4.19_25m6hjymtvcmw3ioyxirlpsioi + '@storybook/node-logger': 6.4.19 + '@storybook/semver': 7.3.2 + '@storybook/store': 6.4.19_sfoxds7t5ydpegc3knd667wn6m + '@types/node': 14.14.33 + '@types/node-fetch': 2.6.1 + '@types/pretty-hrtime': 1.0.1 + '@types/webpack': 4.41.32 + better-opn: 2.1.1 + boxen: 5.1.2 + chalk: 4.1.2 + cli-table3: 0.6.1 + commander: 6.2.1 + compression: 1.7.4 + core-js: 3.25.5 + cpy: 8.1.2 + detect-port: 1.3.0 + express: 4.18.1 + file-system-cache: 1.0.5 + fs-extra: 9.1.0 + globby: 11.1.0 + ip: 1.1.5 + lodash: 4.17.21 + node-fetch: 2.6.7 + pretty-hrtime: 1.0.3 + prompts: 2.4.2 + react: 17.0.2 + react-dom: 17.0.2_react@17.0.2 + regenerator-runtime: 0.13.9 + serve-favicon: 2.5.0 + slash: 3.0.0 + telejson: 5.3.3 + ts-dedent: 2.2.0 + typescript: 4.8.4 + util-deprecate: 1.0.2 + watchpack: 2.3.1 + webpack: 4.46.0_webpack-cli@3.3.12 + ws: 8.5.0 + transitivePeerDependencies: + - '@types/react' + - acorn + - bluebird + - bufferutil + - encoding + - eslint + - supports-color + - utf-8-validate + - vue-template-compiler + - webpack-cli + - webpack-command + dev: true + /@storybook/core-server/6.4.19_2x3ckvxqfngstqwiutxqcrgnby: resolution: {integrity: sha512-bKsUB9f7hl5ya2JXxpIrErmbDQjoH39FVbzYZWjMo4t/b7+Xyi6vYadwyWcqlpUQmis09ZaSMv8L/Tw0TuwLAA==} peerDependencies: @@ -11557,7 +11540,7 @@ packages: - webpack-command dev: true - /@storybook/core-server/6.4.19_fukhgakaronpnxxxb7r2advxsa: + /@storybook/core-server/6.4.19_o7sd63cu7bkdykipq4mkrzuw2i: resolution: {integrity: sha512-bKsUB9f7hl5ya2JXxpIrErmbDQjoH39FVbzYZWjMo4t/b7+Xyi6vYadwyWcqlpUQmis09ZaSMv8L/Tw0TuwLAA==} peerDependencies: '@storybook/builder-webpack5': 6.4.19 @@ -11574,13 +11557,13 @@ packages: optional: true dependencies: '@discoveryjs/json-ext': 0.5.7 - '@storybook/builder-webpack4': 6.4.19_fukhgakaronpnxxxb7r2advxsa + '@storybook/builder-webpack4': 6.4.19_o7sd63cu7bkdykipq4mkrzuw2i '@storybook/core-client': 6.4.19_lb6j7tllhltqtas2n635xqdotu - '@storybook/core-common': 6.4.19_bhvadzvbuq4c4gucumdoppg3by + '@storybook/core-common': 6.4.19_mqzgkamhc7bbbitv65cxtf4gfa '@storybook/core-events': 6.4.19 '@storybook/csf': 0.0.2--canary.87bc651.0 '@storybook/csf-tools': 6.4.19 - '@storybook/manager-webpack4': 6.4.19_fukhgakaronpnxxxb7r2advxsa + '@storybook/manager-webpack4': 6.4.19_o7sd63cu7bkdykipq4mkrzuw2i '@storybook/node-logger': 6.4.19 '@storybook/semver': 7.3.2 '@storybook/store': 6.4.19_sfoxds7t5ydpegc3knd667wn6m @@ -11632,7 +11615,7 @@ packages: - webpack-command dev: true - /@storybook/core-server/6.4.19_kdglyhz445ek5yhe73f5yegd3m: + /@storybook/core-server/6.4.19_yuv5drn7ijijfjkwa6mxoauvrq: resolution: {integrity: sha512-bKsUB9f7hl5ya2JXxpIrErmbDQjoH39FVbzYZWjMo4t/b7+Xyi6vYadwyWcqlpUQmis09ZaSMv8L/Tw0TuwLAA==} peerDependencies: '@storybook/builder-webpack5': 6.4.19 @@ -11649,88 +11632,13 @@ packages: optional: true dependencies: '@discoveryjs/json-ext': 0.5.7 - '@storybook/builder-webpack4': 6.4.19_kdglyhz445ek5yhe73f5yegd3m + '@storybook/builder-webpack4': 6.4.19_yuv5drn7ijijfjkwa6mxoauvrq '@storybook/core-client': 6.4.19_4khy3msxr4lnrhwh6cbg2lwt64 - '@storybook/core-common': 6.4.19_bhvadzvbuq4c4gucumdoppg3by + '@storybook/core-common': 6.4.19_mqzgkamhc7bbbitv65cxtf4gfa '@storybook/core-events': 6.4.19 '@storybook/csf': 0.0.2--canary.87bc651.0 '@storybook/csf-tools': 6.4.19 - '@storybook/manager-webpack4': 6.4.19_kdglyhz445ek5yhe73f5yegd3m - '@storybook/node-logger': 6.4.19 - '@storybook/semver': 7.3.2 - '@storybook/store': 6.4.19_sfoxds7t5ydpegc3knd667wn6m - '@types/node': 14.14.33 - '@types/node-fetch': 2.6.1 - '@types/pretty-hrtime': 1.0.1 - '@types/webpack': 4.41.32 - better-opn: 2.1.1 - boxen: 5.1.2 - chalk: 4.1.2 - cli-table3: 0.6.1 - commander: 6.2.1 - compression: 1.7.4 - core-js: 3.25.5 - cpy: 8.1.2 - detect-port: 1.3.0 - express: 4.18.1 - file-system-cache: 1.0.5 - fs-extra: 9.1.0 - globby: 11.1.0 - ip: 1.1.5 - lodash: 4.17.21 - node-fetch: 2.6.7 - pretty-hrtime: 1.0.3 - prompts: 2.4.2 - react: 17.0.2 - react-dom: 17.0.2_react@17.0.2 - regenerator-runtime: 0.13.9 - serve-favicon: 2.5.0 - slash: 3.0.0 - telejson: 5.3.3 - ts-dedent: 2.2.0 - typescript: 4.8.4 - util-deprecate: 1.0.2 - watchpack: 2.3.1 - webpack: 4.46.0_webpack-cli@3.3.12 - ws: 8.5.0 - transitivePeerDependencies: - - '@types/react' - - acorn - - bluebird - - bufferutil - - encoding - - eslint - - supports-color - - utf-8-validate - - vue-template-compiler - - webpack-cli - - webpack-command - dev: true - - /@storybook/core-server/6.4.19_vrzxlgakqf7u5t766r72d2xf3u: - resolution: {integrity: sha512-bKsUB9f7hl5ya2JXxpIrErmbDQjoH39FVbzYZWjMo4t/b7+Xyi6vYadwyWcqlpUQmis09ZaSMv8L/Tw0TuwLAA==} - peerDependencies: - '@storybook/builder-webpack5': 6.4.19 - '@storybook/manager-webpack5': 6.4.19 - react: ^16.8.0 || ^17.0.0 - react-dom: ^16.8.0 || ^17.0.0 - typescript: '*' - peerDependenciesMeta: - '@storybook/builder-webpack5': - optional: true - '@storybook/manager-webpack5': - optional: true - typescript: - optional: true - dependencies: - '@discoveryjs/json-ext': 0.5.7 - '@storybook/builder-webpack4': 6.4.19_vrzxlgakqf7u5t766r72d2xf3u - '@storybook/core-client': 6.4.19_4khy3msxr4lnrhwh6cbg2lwt64 - '@storybook/core-common': 6.4.19_bhvadzvbuq4c4gucumdoppg3by - '@storybook/core-events': 6.4.19 - '@storybook/csf': 0.0.2--canary.87bc651.0 - '@storybook/csf-tools': 6.4.19 - '@storybook/manager-webpack4': 6.4.19_vrzxlgakqf7u5t766r72d2xf3u + '@storybook/manager-webpack4': 6.4.19_yuv5drn7ijijfjkwa6mxoauvrq '@storybook/node-logger': 6.4.19 '@storybook/semver': 7.3.2 '@storybook/store': 6.4.19_sfoxds7t5ydpegc3knd667wn6m @@ -11895,77 +11803,7 @@ packages: - webpack-command dev: true - /@storybook/core/6.4.19_hxw5eumcvhbkoh74pcqihkovhi: - resolution: {integrity: sha512-55LOQ/h/kf1jMhjN85t/pIEdIwWEG9yV7bdwv3niVvmoypCxyyjn9/QNK0RKYAeDSUtdm6FVoJ6k5CpxWz2d8w==} - peerDependencies: - '@storybook/builder-webpack5': 6.4.19 - react: ^16.8.0 || ^17.0.0 - react-dom: ^16.8.0 || ^17.0.0 - typescript: '*' - webpack: '*' - peerDependenciesMeta: - '@storybook/builder-webpack5': - optional: true - typescript: - optional: true - dependencies: - '@storybook/core-client': 6.4.19_4khy3msxr4lnrhwh6cbg2lwt64 - '@storybook/core-server': 6.4.19_kdglyhz445ek5yhe73f5yegd3m - react: 17.0.2 - react-dom: 17.0.2_react@17.0.2 - typescript: 4.8.4 - webpack: 4.46.0_webpack-cli@3.3.12 - transitivePeerDependencies: - - '@storybook/manager-webpack5' - - '@types/react' - - acorn - - bluebird - - bufferutil - - encoding - - eslint - - supports-color - - utf-8-validate - - vue-template-compiler - - webpack-cli - - webpack-command - dev: true - - /@storybook/core/6.4.19_p3r3fihtzjpxjnglz4l5qyfmaa: - resolution: {integrity: sha512-55LOQ/h/kf1jMhjN85t/pIEdIwWEG9yV7bdwv3niVvmoypCxyyjn9/QNK0RKYAeDSUtdm6FVoJ6k5CpxWz2d8w==} - peerDependencies: - '@storybook/builder-webpack5': 6.4.19 - react: ^16.8.0 || ^17.0.0 - react-dom: ^16.8.0 || ^17.0.0 - typescript: '*' - webpack: '*' - peerDependenciesMeta: - '@storybook/builder-webpack5': - optional: true - typescript: - optional: true - dependencies: - '@storybook/core-client': 6.4.19_lb6j7tllhltqtas2n635xqdotu - '@storybook/core-server': 6.4.19_fukhgakaronpnxxxb7r2advxsa - react: 17.0.2 - react-dom: 17.0.2_react@17.0.2 - typescript: 4.8.4 - webpack: 4.46.0_webpack-cli@3.3.12 - transitivePeerDependencies: - - '@storybook/manager-webpack5' - - '@types/react' - - acorn - - bluebird - - bufferutil - - encoding - - eslint - - supports-color - - utf-8-validate - - vue-template-compiler - - webpack-cli - - webpack-command - dev: true - - /@storybook/core/6.4.19_uog2ckjumoqoyqssiweskrah3a: + /@storybook/core/6.4.19_6cqelbxt6lx74wa4u7ecuuny7i: resolution: {integrity: sha512-55LOQ/h/kf1jMhjN85t/pIEdIwWEG9yV7bdwv3niVvmoypCxyyjn9/QNK0RKYAeDSUtdm6FVoJ6k5CpxWz2d8w==} peerDependencies: '@storybook/builder-webpack5': 6.4.19 @@ -11980,7 +11818,7 @@ packages: optional: true dependencies: '@storybook/core-client': 6.4.19_nj5p77xh6arla3uyzyeb3fmasm - '@storybook/core-server': 6.4.19_vrzxlgakqf7u5t766r72d2xf3u + '@storybook/core-server': 6.4.19_yuv5drn7ijijfjkwa6mxoauvrq react: 17.0.2 react-dom: 17.0.2_react@17.0.2 typescript: 4.8.4 @@ -12000,6 +11838,76 @@ packages: - webpack-command dev: true + /@storybook/core/6.4.19_jqd6s4xt6kov4uvvwofgl2yngm: + resolution: {integrity: sha512-55LOQ/h/kf1jMhjN85t/pIEdIwWEG9yV7bdwv3niVvmoypCxyyjn9/QNK0RKYAeDSUtdm6FVoJ6k5CpxWz2d8w==} + peerDependencies: + '@storybook/builder-webpack5': 6.4.19 + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + typescript: '*' + webpack: '*' + peerDependenciesMeta: + '@storybook/builder-webpack5': + optional: true + typescript: + optional: true + dependencies: + '@storybook/core-client': 6.4.19_lb6j7tllhltqtas2n635xqdotu + '@storybook/core-server': 6.4.19_o7sd63cu7bkdykipq4mkrzuw2i + react: 17.0.2 + react-dom: 17.0.2_react@17.0.2 + typescript: 4.8.4 + webpack: 4.46.0_webpack-cli@3.3.12 + transitivePeerDependencies: + - '@storybook/manager-webpack5' + - '@types/react' + - acorn + - bluebird + - bufferutil + - encoding + - eslint + - supports-color + - utf-8-validate + - vue-template-compiler + - webpack-cli + - webpack-command + dev: true + + /@storybook/core/6.4.19_lyggee6zfyjj6ezzf7jaxeljxi: + resolution: {integrity: sha512-55LOQ/h/kf1jMhjN85t/pIEdIwWEG9yV7bdwv3niVvmoypCxyyjn9/QNK0RKYAeDSUtdm6FVoJ6k5CpxWz2d8w==} + peerDependencies: + '@storybook/builder-webpack5': 6.4.19 + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + typescript: '*' + webpack: '*' + peerDependenciesMeta: + '@storybook/builder-webpack5': + optional: true + typescript: + optional: true + dependencies: + '@storybook/core-client': 6.4.19_4khy3msxr4lnrhwh6cbg2lwt64 + '@storybook/core-server': 6.4.19_25m6hjymtvcmw3ioyxirlpsioi + react: 17.0.2 + react-dom: 17.0.2_react@17.0.2 + typescript: 4.8.4 + webpack: 4.46.0_webpack-cli@3.3.12 + transitivePeerDependencies: + - '@storybook/manager-webpack5' + - '@types/react' + - acorn + - bluebird + - bufferutil + - encoding + - eslint + - supports-color + - utf-8-validate + - vue-template-compiler + - webpack-cli + - webpack-command + dev: true + /@storybook/core/6.4.19_ybd46eyevy5nesjyz6rrqmwwmu: resolution: {integrity: sha512-55LOQ/h/kf1jMhjN85t/pIEdIwWEG9yV7bdwv3niVvmoypCxyyjn9/QNK0RKYAeDSUtdm6FVoJ6k5CpxWz2d8w==} peerDependencies: @@ -12066,6 +11974,67 @@ packages: lodash: 4.17.21 dev: true + /@storybook/manager-webpack4/6.4.19_25m6hjymtvcmw3ioyxirlpsioi: + resolution: {integrity: sha512-R8ugZjTYqXvlc6gDOcw909L65sIleOmIJLZR+N6/H85MivGXHu39jOwONqB7tVACufRty4FNecn8tEiQL2SAKA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@babel/core': 7.17.8 + '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.17.8 + '@babel/preset-react': 7.16.7_@babel+core@7.17.8 + '@storybook/addons': 6.4.19_sfoxds7t5ydpegc3knd667wn6m + '@storybook/core-client': 6.4.19_4khy3msxr4lnrhwh6cbg2lwt64 + '@storybook/core-common': 6.4.19_mqzgkamhc7bbbitv65cxtf4gfa + '@storybook/node-logger': 6.4.19 + '@storybook/theming': 6.4.19_sfoxds7t5ydpegc3knd667wn6m + '@storybook/ui': 6.4.19_hiunvzosbwliizyirxfy6hjyim + '@types/node': 14.14.33 + '@types/webpack': 4.41.32 + babel-loader: 8.2.3_w4x3pzrj2omidyjy5w3nzug7xy + case-sensitive-paths-webpack-plugin: 2.4.0 + chalk: 4.1.2 + core-js: 3.25.5 + css-loader: 3.6.0_webpack@4.46.0 + express: 4.18.1 + file-loader: 6.2.0_webpack@4.46.0 + file-system-cache: 1.0.5 + find-up: 5.0.0 + fs-extra: 9.1.0 + html-webpack-plugin: 4.5.2_webpack@4.46.0 + node-fetch: 2.6.7 + pnp-webpack-plugin: 1.6.4_typescript@4.8.4 + react: 17.0.2 + react-dom: 17.0.2_react@17.0.2 + read-pkg-up: 7.0.1 + regenerator-runtime: 0.13.9 + resolve-from: 5.0.0 + style-loader: 1.3.0_webpack@4.46.0 + telejson: 5.3.3 + terser-webpack-plugin: 4.2.3_acorn@8.8.1+webpack@4.46.0 + ts-dedent: 2.2.0 + typescript: 4.8.4 + url-loader: 4.1.1_lit45vopotvaqup7lrvlnvtxwy + util-deprecate: 1.0.2 + webpack: 4.46.0_webpack-cli@3.3.12 + webpack-dev-middleware: 3.7.3_webpack@4.46.0 + webpack-virtual-modules: 0.2.2 + transitivePeerDependencies: + - '@types/react' + - acorn + - bluebird + - encoding + - eslint + - supports-color + - vue-template-compiler + - webpack-cli + - webpack-command + dev: true + /@storybook/manager-webpack4/6.4.19_3n4gsnmxucj3bywv6syggoiztm: resolution: {integrity: sha512-R8ugZjTYqXvlc6gDOcw909L65sIleOmIJLZR+N6/H85MivGXHu39jOwONqB7tVACufRty4FNecn8tEiQL2SAKA==} peerDependencies: @@ -12188,7 +12157,7 @@ packages: - webpack-command dev: true - /@storybook/manager-webpack4/6.4.19_fukhgakaronpnxxxb7r2advxsa: + /@storybook/manager-webpack4/6.4.19_o7sd63cu7bkdykipq4mkrzuw2i: resolution: {integrity: sha512-R8ugZjTYqXvlc6gDOcw909L65sIleOmIJLZR+N6/H85MivGXHu39jOwONqB7tVACufRty4FNecn8tEiQL2SAKA==} peerDependencies: react: ^16.8.0 || ^17.0.0 @@ -12203,7 +12172,7 @@ packages: '@babel/preset-react': 7.16.7_@babel+core@7.17.8 '@storybook/addons': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/core-client': 6.4.19_lb6j7tllhltqtas2n635xqdotu - '@storybook/core-common': 6.4.19_bhvadzvbuq4c4gucumdoppg3by + '@storybook/core-common': 6.4.19_mqzgkamhc7bbbitv65cxtf4gfa '@storybook/node-logger': 6.4.19 '@storybook/theming': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/ui': 6.4.19_sfoxds7t5ydpegc3knd667wn6m @@ -12249,7 +12218,7 @@ packages: - webpack-command dev: true - /@storybook/manager-webpack4/6.4.19_kdglyhz445ek5yhe73f5yegd3m: + /@storybook/manager-webpack4/6.4.19_yuv5drn7ijijfjkwa6mxoauvrq: resolution: {integrity: sha512-R8ugZjTYqXvlc6gDOcw909L65sIleOmIJLZR+N6/H85MivGXHu39jOwONqB7tVACufRty4FNecn8tEiQL2SAKA==} peerDependencies: react: ^16.8.0 || ^17.0.0 @@ -12264,68 +12233,7 @@ packages: '@babel/preset-react': 7.16.7_@babel+core@7.17.8 '@storybook/addons': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/core-client': 6.4.19_4khy3msxr4lnrhwh6cbg2lwt64 - '@storybook/core-common': 6.4.19_bhvadzvbuq4c4gucumdoppg3by - '@storybook/node-logger': 6.4.19 - '@storybook/theming': 6.4.19_sfoxds7t5ydpegc3knd667wn6m - '@storybook/ui': 6.4.19_hiunvzosbwliizyirxfy6hjyim - '@types/node': 14.14.33 - '@types/webpack': 4.41.32 - babel-loader: 8.2.3_w4x3pzrj2omidyjy5w3nzug7xy - case-sensitive-paths-webpack-plugin: 2.4.0 - chalk: 4.1.2 - core-js: 3.25.5 - css-loader: 3.6.0_webpack@4.46.0 - express: 4.18.1 - file-loader: 6.2.0_webpack@4.46.0 - file-system-cache: 1.0.5 - find-up: 5.0.0 - fs-extra: 9.1.0 - html-webpack-plugin: 4.5.2_webpack@4.46.0 - node-fetch: 2.6.7 - pnp-webpack-plugin: 1.6.4_typescript@4.8.4 - react: 17.0.2 - react-dom: 17.0.2_react@17.0.2 - read-pkg-up: 7.0.1 - regenerator-runtime: 0.13.9 - resolve-from: 5.0.0 - style-loader: 1.3.0_webpack@4.46.0 - telejson: 5.3.3 - terser-webpack-plugin: 4.2.3_acorn@8.8.1+webpack@4.46.0 - ts-dedent: 2.2.0 - typescript: 4.8.4 - url-loader: 4.1.1_lit45vopotvaqup7lrvlnvtxwy - util-deprecate: 1.0.2 - webpack: 4.46.0_webpack-cli@3.3.12 - webpack-dev-middleware: 3.7.3_webpack@4.46.0 - webpack-virtual-modules: 0.2.2 - transitivePeerDependencies: - - '@types/react' - - acorn - - bluebird - - encoding - - eslint - - supports-color - - vue-template-compiler - - webpack-cli - - webpack-command - dev: true - - /@storybook/manager-webpack4/6.4.19_vrzxlgakqf7u5t766r72d2xf3u: - resolution: {integrity: sha512-R8ugZjTYqXvlc6gDOcw909L65sIleOmIJLZR+N6/H85MivGXHu39jOwONqB7tVACufRty4FNecn8tEiQL2SAKA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 - react-dom: ^16.8.0 || ^17.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@babel/core': 7.17.8 - '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.17.8 - '@babel/preset-react': 7.16.7_@babel+core@7.17.8 - '@storybook/addons': 6.4.19_sfoxds7t5ydpegc3knd667wn6m - '@storybook/core-client': 6.4.19_4khy3msxr4lnrhwh6cbg2lwt64 - '@storybook/core-common': 6.4.19_bhvadzvbuq4c4gucumdoppg3by + '@storybook/core-common': 6.4.19_mqzgkamhc7bbbitv65cxtf4gfa '@storybook/node-logger': 6.4.19 '@storybook/theming': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/ui': 6.4.19_hiunvzosbwliizyirxfy6hjyim @@ -12557,7 +12465,7 @@ packages: - webpack-plugin-serve dev: true - /@storybook/react/6.4.19_glozp6fblhaty2oacwbjl7ao2i: + /@storybook/react/6.4.19_cqdgeqmmrux6joug3kc73q4l6m: resolution: {integrity: sha512-5b3i8jkVrjQGmcxxxXwCduHPIh+cluWkfeweKeQOe+lW4BR8fuUICo3AMLrYPAtB/UcaJyYkIYmTvF2mkfepFA==} engines: {node: '>=10.13.0'} hasBin: true @@ -12577,8 +12485,8 @@ packages: '@babel/preset-react': 7.16.7_@babel+core@7.17.8 '@pmmmwh/react-refresh-webpack-plugin': 0.5.1_a3gyllrqvxpec3fpybsrposvju '@storybook/addons': 6.4.19_sfoxds7t5ydpegc3knd667wn6m - '@storybook/core': 6.4.19_p3r3fihtzjpxjnglz4l5qyfmaa - '@storybook/core-common': 6.4.19_bhvadzvbuq4c4gucumdoppg3by + '@storybook/core': 6.4.19_lyggee6zfyjj6ezzf7jaxeljxi + '@storybook/core-common': 6.4.19_mqzgkamhc7bbbitv65cxtf4gfa '@storybook/csf': 0.0.2--canary.87bc651.0 '@storybook/node-logger': 6.4.19 '@storybook/react-docgen-typescript-plugin': 1.0.2-canary.253f8c1.0_lasgyenclx45ngbljrbo537mpe @@ -12622,7 +12530,7 @@ packages: - webpack-plugin-serve dev: true - /@storybook/react/6.4.19_oycjqkyefi4akx2twppuux3udq: + /@storybook/react/6.4.19_pjugpuchrb7ea5kuxwnxihy6zq: resolution: {integrity: sha512-5b3i8jkVrjQGmcxxxXwCduHPIh+cluWkfeweKeQOe+lW4BR8fuUICo3AMLrYPAtB/UcaJyYkIYmTvF2mkfepFA==} engines: {node: '>=10.13.0'} hasBin: true @@ -12642,8 +12550,8 @@ packages: '@babel/preset-react': 7.16.7_@babel+core@7.17.8 '@pmmmwh/react-refresh-webpack-plugin': 0.5.1_a3gyllrqvxpec3fpybsrposvju '@storybook/addons': 6.4.19_sfoxds7t5ydpegc3knd667wn6m - '@storybook/core': 6.4.19_hxw5eumcvhbkoh74pcqihkovhi - '@storybook/core-common': 6.4.19_bhvadzvbuq4c4gucumdoppg3by + '@storybook/core': 6.4.19_jqd6s4xt6kov4uvvwofgl2yngm + '@storybook/core-common': 6.4.19_mqzgkamhc7bbbitv65cxtf4gfa '@storybook/csf': 0.0.2--canary.87bc651.0 '@storybook/node-logger': 6.4.19 '@storybook/react-docgen-typescript-plugin': 1.0.2-canary.253f8c1.0_lasgyenclx45ngbljrbo537mpe @@ -13873,7 +13781,7 @@ packages: optional: true dependencies: '@typescript-eslint/experimental-utils': 4.33.0_3rubbgt5ekhqrcgx4uwls3neim - '@typescript-eslint/parser': 4.33.0_phzabm2hax2olcsrhm4n4aik5y + '@typescript-eslint/parser': 4.33.0_yygwinqv3a2io74xmwofqb7uka '@typescript-eslint/scope-manager': 4.33.0 debug: 4.3.4 eslint: 7.32.0 @@ -13887,7 +13795,7 @@ packages: - supports-color dev: true - /@typescript-eslint/eslint-plugin/5.43.0_72qpevtmaezorvyo7j2xoo37sy: + /@typescript-eslint/eslint-plugin/5.43.0_7f3f6qw7x62gbtvc33ujooybme: resolution: {integrity: sha512-wNPzG+eDR6+hhW4yobEmpR36jrqqQv1vxBq5LJO3fBAktjkvekfr4BRl+3Fn1CM/A+s8/EiGUbOMDoYqWdbtXA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -13898,12 +13806,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/parser': 5.43.0_yd7pksmmyt33nzyuulu63alu3m + '@typescript-eslint/parser': 5.43.0_yygwinqv3a2io74xmwofqb7uka '@typescript-eslint/scope-manager': 5.43.0 - '@typescript-eslint/type-utils': 5.43.0_yd7pksmmyt33nzyuulu63alu3m - '@typescript-eslint/utils': 5.43.0_yd7pksmmyt33nzyuulu63alu3m + '@typescript-eslint/type-utils': 5.43.0_yygwinqv3a2io74xmwofqb7uka + '@typescript-eslint/utils': 5.43.0_yygwinqv3a2io74xmwofqb7uka debug: 4.3.4 - eslint: 8.2.0 + eslint: 8.32.0 ignore: 5.2.0 natural-compare-lite: 1.4.0 regexpp: 3.2.0 @@ -13912,114 +13820,6 @@ packages: typescript: 4.8.4 transitivePeerDependencies: - supports-color - dev: true - - /@typescript-eslint/eslint-plugin/5.43.0_77yfnzmbnonej4k3inkgdy4i5u: - resolution: {integrity: sha512-wNPzG+eDR6+hhW4yobEmpR36jrqqQv1vxBq5LJO3fBAktjkvekfr4BRl+3Fn1CM/A+s8/EiGUbOMDoYqWdbtXA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/parser': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/parser': 5.43.0_iqokrdhiz7bccawj5qurem2l4e - '@typescript-eslint/scope-manager': 5.43.0 - '@typescript-eslint/type-utils': 5.43.0_iqokrdhiz7bccawj5qurem2l4e - '@typescript-eslint/utils': 5.43.0_iqokrdhiz7bccawj5qurem2l4e - debug: 4.3.4 - eslint: 8.12.0 - ignore: 5.2.0 - natural-compare-lite: 1.4.0 - regexpp: 3.2.0 - semver: 7.3.8 - tsutils: 3.21.0_typescript@4.8.4 - typescript: 4.8.4 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/eslint-plugin/5.43.0_hhpcbb6wqnhvo6wpcctutdxelq: - resolution: {integrity: sha512-wNPzG+eDR6+hhW4yobEmpR36jrqqQv1vxBq5LJO3fBAktjkvekfr4BRl+3Fn1CM/A+s8/EiGUbOMDoYqWdbtXA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/parser': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/parser': 5.43.0_z4bbprzjrhnsfa24uvmcbu7f5q - '@typescript-eslint/scope-manager': 5.43.0 - '@typescript-eslint/type-utils': 5.43.0_z4bbprzjrhnsfa24uvmcbu7f5q - '@typescript-eslint/utils': 5.43.0_z4bbprzjrhnsfa24uvmcbu7f5q - debug: 4.3.4 - eslint: 8.25.0 - ignore: 5.2.0 - natural-compare-lite: 1.4.0 - regexpp: 3.2.0 - semver: 7.3.8 - tsutils: 3.21.0_typescript@4.8.4 - typescript: 4.8.4 - transitivePeerDependencies: - - supports-color - - /@typescript-eslint/eslint-plugin/5.43.0_ol5heeoi7wmwb4tenztnb7jlze: - resolution: {integrity: sha512-wNPzG+eDR6+hhW4yobEmpR36jrqqQv1vxBq5LJO3fBAktjkvekfr4BRl+3Fn1CM/A+s8/EiGUbOMDoYqWdbtXA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/parser': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/parser': 5.43.0_zksrc6ykdxhogxjbhb5axiabwi - '@typescript-eslint/scope-manager': 5.43.0 - '@typescript-eslint/type-utils': 5.43.0_zksrc6ykdxhogxjbhb5axiabwi - '@typescript-eslint/utils': 5.43.0_zksrc6ykdxhogxjbhb5axiabwi - debug: 4.3.4 - eslint: 8.28.0 - ignore: 5.2.0 - natural-compare-lite: 1.4.0 - regexpp: 3.2.0 - semver: 7.3.8 - tsutils: 3.21.0_typescript@4.8.4 - typescript: 4.8.4 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/eslint-plugin/5.43.0_xi2bidqtjw6phi5qq54cbyxqtu: - resolution: {integrity: sha512-wNPzG+eDR6+hhW4yobEmpR36jrqqQv1vxBq5LJO3fBAktjkvekfr4BRl+3Fn1CM/A+s8/EiGUbOMDoYqWdbtXA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/parser': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/parser': 5.43.0_himlt4eddny2rsb5zkuydvuf7u - '@typescript-eslint/scope-manager': 5.43.0 - '@typescript-eslint/type-utils': 5.43.0_himlt4eddny2rsb5zkuydvuf7u - '@typescript-eslint/utils': 5.43.0_himlt4eddny2rsb5zkuydvuf7u - debug: 4.3.4 - eslint: 8.11.0 - ignore: 5.2.0 - natural-compare-lite: 1.4.0 - regexpp: 3.2.0 - semver: 7.3.8 - tsutils: 3.21.0_typescript@4.8.4 - typescript: 4.8.4 - transitivePeerDependencies: - - supports-color - dev: true /@typescript-eslint/experimental-utils/2.34.0_3rubbgt5ekhqrcgx4uwls3neim: resolution: {integrity: sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==} @@ -14037,7 +13837,7 @@ packages: - typescript dev: true - /@typescript-eslint/experimental-utils/2.34.0_iqokrdhiz7bccawj5qurem2l4e: + /@typescript-eslint/experimental-utils/2.34.0_yygwinqv3a2io74xmwofqb7uka: resolution: {integrity: sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==} engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} peerDependencies: @@ -14045,23 +13845,7 @@ packages: dependencies: '@types/json-schema': 7.0.9 '@typescript-eslint/typescript-estree': 2.34.0_typescript@4.8.4 - eslint: 8.12.0 - eslint-scope: 5.1.1 - eslint-utils: 2.1.0 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /@typescript-eslint/experimental-utils/2.34.0_z4bbprzjrhnsfa24uvmcbu7f5q: - resolution: {integrity: sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==} - engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} - peerDependencies: - eslint: '*' - dependencies: - '@types/json-schema': 7.0.9 - '@typescript-eslint/typescript-estree': 2.34.0_typescript@4.8.4 - eslint: 8.25.0 + eslint: 8.32.0 eslint-scope: 5.1.1 eslint-utils: 2.1.0 transitivePeerDependencies: @@ -14087,32 +13871,19 @@ packages: - typescript dev: true - /@typescript-eslint/experimental-utils/5.43.0_z4bbprzjrhnsfa24uvmcbu7f5q: + /@typescript-eslint/experimental-utils/5.43.0_yygwinqv3a2io74xmwofqb7uka: resolution: {integrity: sha512-WkT637CumTJbm/hRbFfnHBMgfUYTKr08LitVsD7gQId7bi6rnkx3pu3jac67lmp5ObW4MpJ9SNFZAIOUB/Qbsw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.43.0_z4bbprzjrhnsfa24uvmcbu7f5q - eslint: 8.25.0 + '@typescript-eslint/utils': 5.43.0_yygwinqv3a2io74xmwofqb7uka + eslint: 8.32.0 transitivePeerDependencies: - supports-color - typescript - /@typescript-eslint/experimental-utils/5.43.0_zksrc6ykdxhogxjbhb5axiabwi: - resolution: {integrity: sha512-WkT637CumTJbm/hRbFfnHBMgfUYTKr08LitVsD7gQId7bi6rnkx3pu3jac67lmp5ObW4MpJ9SNFZAIOUB/Qbsw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@typescript-eslint/utils': 5.43.0_zksrc6ykdxhogxjbhb5axiabwi - eslint: 8.28.0 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /@typescript-eslint/parser/4.33.0_phzabm2hax2olcsrhm4n4aik5y: + /@typescript-eslint/parser/4.33.0_yygwinqv3a2io74xmwofqb7uka: resolution: {integrity: sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==} engines: {node: ^10.12.0 || >=12.0.0} peerDependencies: @@ -14126,13 +13897,13 @@ packages: '@typescript-eslint/types': 4.33.0 '@typescript-eslint/typescript-estree': 4.33.0_typescript@4.8.4 debug: 4.3.4 - eslint: 5.16.0 + eslint: 8.32.0 typescript: 4.8.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser/5.15.0_z4bbprzjrhnsfa24uvmcbu7f5q: + /@typescript-eslint/parser/5.15.0_yygwinqv3a2io74xmwofqb7uka: resolution: {integrity: sha512-NGAYP/+RDM2sVfmKiKOCgJYPstAO40vPAgACoWPO/+yoYKSgAXIFaBKsV8P0Cc7fwKgvj27SjRNX4L7f4/jCKQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -14146,13 +13917,13 @@ packages: '@typescript-eslint/types': 5.15.0 '@typescript-eslint/typescript-estree': 5.15.0_typescript@4.8.4 debug: 4.3.3 - eslint: 8.25.0 + eslint: 8.32.0 typescript: 4.8.4 transitivePeerDependencies: - supports-color dev: false - /@typescript-eslint/parser/5.43.0_himlt4eddny2rsb5zkuydvuf7u: + /@typescript-eslint/parser/5.43.0_yygwinqv3a2io74xmwofqb7uka: resolution: {integrity: sha512-2iHUK2Lh7PwNUlhFxxLI2haSDNyXvebBO9izhjhMoDC+S3XI9qt2DGFUsiJ89m2k7gGYch2aEpYqV5F/+nwZug==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -14166,86 +13937,7 @@ packages: '@typescript-eslint/types': 5.43.0 '@typescript-eslint/typescript-estree': 5.43.0_typescript@4.8.4 debug: 4.3.4 - eslint: 8.11.0 - typescript: 4.8.4 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/parser/5.43.0_iqokrdhiz7bccawj5qurem2l4e: - resolution: {integrity: sha512-2iHUK2Lh7PwNUlhFxxLI2haSDNyXvebBO9izhjhMoDC+S3XI9qt2DGFUsiJ89m2k7gGYch2aEpYqV5F/+nwZug==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/scope-manager': 5.43.0 - '@typescript-eslint/types': 5.43.0 - '@typescript-eslint/typescript-estree': 5.43.0_typescript@4.8.4 - debug: 4.3.4 - eslint: 8.12.0 - typescript: 4.8.4 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/parser/5.43.0_yd7pksmmyt33nzyuulu63alu3m: - resolution: {integrity: sha512-2iHUK2Lh7PwNUlhFxxLI2haSDNyXvebBO9izhjhMoDC+S3XI9qt2DGFUsiJ89m2k7gGYch2aEpYqV5F/+nwZug==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/scope-manager': 5.43.0 - '@typescript-eslint/types': 5.43.0 - '@typescript-eslint/typescript-estree': 5.43.0_typescript@4.8.4 - debug: 4.3.4 - eslint: 8.2.0 - typescript: 4.8.4 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/parser/5.43.0_z4bbprzjrhnsfa24uvmcbu7f5q: - resolution: {integrity: sha512-2iHUK2Lh7PwNUlhFxxLI2haSDNyXvebBO9izhjhMoDC+S3XI9qt2DGFUsiJ89m2k7gGYch2aEpYqV5F/+nwZug==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/scope-manager': 5.43.0 - '@typescript-eslint/types': 5.43.0 - '@typescript-eslint/typescript-estree': 5.43.0_typescript@4.8.4 - debug: 4.3.4 - eslint: 8.25.0 - typescript: 4.8.4 - transitivePeerDependencies: - - supports-color - - /@typescript-eslint/parser/5.43.0_zksrc6ykdxhogxjbhb5axiabwi: - resolution: {integrity: sha512-2iHUK2Lh7PwNUlhFxxLI2haSDNyXvebBO9izhjhMoDC+S3XI9qt2DGFUsiJ89m2k7gGYch2aEpYqV5F/+nwZug==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/scope-manager': 5.43.0 - '@typescript-eslint/types': 5.43.0 - '@typescript-eslint/typescript-estree': 5.43.0_typescript@4.8.4 - debug: 4.3.4 - eslint: 8.28.0 + eslint: 8.32.0 typescript: 4.8.4 transitivePeerDependencies: - supports-color @@ -14273,7 +13965,7 @@ packages: '@typescript-eslint/types': 5.43.0 '@typescript-eslint/visitor-keys': 5.43.0 - /@typescript-eslint/type-utils/5.43.0_himlt4eddny2rsb5zkuydvuf7u: + /@typescript-eslint/type-utils/5.43.0_yygwinqv3a2io74xmwofqb7uka: resolution: {integrity: sha512-K21f+KY2/VvYggLf5Pk4tgBOPs2otTaIHy2zjclo7UZGLyFH86VfUOm5iq+OtDtxq/Zwu2I3ujDBykVW4Xtmtg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -14284,93 +13976,13 @@ packages: optional: true dependencies: '@typescript-eslint/typescript-estree': 5.43.0_typescript@4.8.4 - '@typescript-eslint/utils': 5.43.0_himlt4eddny2rsb5zkuydvuf7u + '@typescript-eslint/utils': 5.43.0_yygwinqv3a2io74xmwofqb7uka debug: 4.3.4 - eslint: 8.11.0 + eslint: 8.32.0 tsutils: 3.21.0_typescript@4.8.4 typescript: 4.8.4 transitivePeerDependencies: - supports-color - dev: true - - /@typescript-eslint/type-utils/5.43.0_iqokrdhiz7bccawj5qurem2l4e: - resolution: {integrity: sha512-K21f+KY2/VvYggLf5Pk4tgBOPs2otTaIHy2zjclo7UZGLyFH86VfUOm5iq+OtDtxq/Zwu2I3ujDBykVW4Xtmtg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '*' - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/typescript-estree': 5.43.0_typescript@4.8.4 - '@typescript-eslint/utils': 5.43.0_iqokrdhiz7bccawj5qurem2l4e - debug: 4.3.4 - eslint: 8.12.0 - tsutils: 3.21.0_typescript@4.8.4 - typescript: 4.8.4 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/type-utils/5.43.0_yd7pksmmyt33nzyuulu63alu3m: - resolution: {integrity: sha512-K21f+KY2/VvYggLf5Pk4tgBOPs2otTaIHy2zjclo7UZGLyFH86VfUOm5iq+OtDtxq/Zwu2I3ujDBykVW4Xtmtg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '*' - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/typescript-estree': 5.43.0_typescript@4.8.4 - '@typescript-eslint/utils': 5.43.0_yd7pksmmyt33nzyuulu63alu3m - debug: 4.3.4 - eslint: 8.2.0 - tsutils: 3.21.0_typescript@4.8.4 - typescript: 4.8.4 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/type-utils/5.43.0_z4bbprzjrhnsfa24uvmcbu7f5q: - resolution: {integrity: sha512-K21f+KY2/VvYggLf5Pk4tgBOPs2otTaIHy2zjclo7UZGLyFH86VfUOm5iq+OtDtxq/Zwu2I3ujDBykVW4Xtmtg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '*' - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/typescript-estree': 5.43.0_typescript@4.8.4 - '@typescript-eslint/utils': 5.43.0_z4bbprzjrhnsfa24uvmcbu7f5q - debug: 4.3.4 - eslint: 8.25.0 - tsutils: 3.21.0_typescript@4.8.4 - typescript: 4.8.4 - transitivePeerDependencies: - - supports-color - - /@typescript-eslint/type-utils/5.43.0_zksrc6ykdxhogxjbhb5axiabwi: - resolution: {integrity: sha512-K21f+KY2/VvYggLf5Pk4tgBOPs2otTaIHy2zjclo7UZGLyFH86VfUOm5iq+OtDtxq/Zwu2I3ujDBykVW4Xtmtg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '*' - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/typescript-estree': 5.43.0_typescript@4.8.4 - '@typescript-eslint/utils': 5.43.0_zksrc6ykdxhogxjbhb5axiabwi - debug: 4.3.4 - eslint: 8.28.0 - tsutils: 3.21.0_typescript@4.8.4 - typescript: 4.8.4 - transitivePeerDependencies: - - supports-color - dev: true /@typescript-eslint/types/4.33.0: resolution: {integrity: sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==} @@ -14469,7 +14081,7 @@ packages: transitivePeerDependencies: - supports-color - /@typescript-eslint/utils/5.15.0_z4bbprzjrhnsfa24uvmcbu7f5q: + /@typescript-eslint/utils/5.15.0_yygwinqv3a2io74xmwofqb7uka: resolution: {integrity: sha512-081rWu2IPKOgTOhHUk/QfxuFog8m4wxW43sXNOMSCdh578tGJ1PAaWPsj42LOa7pguh173tNlMigsbrHvh/mtA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -14479,15 +14091,15 @@ packages: '@typescript-eslint/scope-manager': 5.15.0 '@typescript-eslint/types': 5.15.0 '@typescript-eslint/typescript-estree': 5.15.0_typescript@4.8.4 - eslint: 8.25.0 + eslint: 8.32.0 eslint-scope: 5.1.1 - eslint-utils: 3.0.0_eslint@8.25.0 + eslint-utils: 3.0.0_eslint@8.32.0 transitivePeerDependencies: - supports-color - typescript dev: false - /@typescript-eslint/utils/5.43.0_himlt4eddny2rsb5zkuydvuf7u: + /@typescript-eslint/utils/5.43.0_yygwinqv3a2io74xmwofqb7uka: resolution: {integrity: sha512-8nVpA6yX0sCjf7v/NDfeaOlyaIIqL7OaIGOWSPFqUKK59Gnumd3Wa+2l8oAaYO2lk0sO+SbWFWRSvhu8gLGv4A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -14498,93 +14110,13 @@ packages: '@typescript-eslint/scope-manager': 5.43.0 '@typescript-eslint/types': 5.43.0 '@typescript-eslint/typescript-estree': 5.43.0_typescript@4.8.4 - eslint: 8.11.0 + eslint: 8.32.0 eslint-scope: 5.1.1 - eslint-utils: 3.0.0_eslint@8.11.0 + eslint-utils: 3.0.0_eslint@8.32.0 semver: 7.3.8 transitivePeerDependencies: - supports-color - typescript - dev: true - - /@typescript-eslint/utils/5.43.0_iqokrdhiz7bccawj5qurem2l4e: - resolution: {integrity: sha512-8nVpA6yX0sCjf7v/NDfeaOlyaIIqL7OaIGOWSPFqUKK59Gnumd3Wa+2l8oAaYO2lk0sO+SbWFWRSvhu8gLGv4A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@types/json-schema': 7.0.9 - '@types/semver': 7.3.12 - '@typescript-eslint/scope-manager': 5.43.0 - '@typescript-eslint/types': 5.43.0 - '@typescript-eslint/typescript-estree': 5.43.0_typescript@4.8.4 - eslint: 8.12.0 - eslint-scope: 5.1.1 - eslint-utils: 3.0.0_eslint@8.12.0 - semver: 7.3.8 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /@typescript-eslint/utils/5.43.0_yd7pksmmyt33nzyuulu63alu3m: - resolution: {integrity: sha512-8nVpA6yX0sCjf7v/NDfeaOlyaIIqL7OaIGOWSPFqUKK59Gnumd3Wa+2l8oAaYO2lk0sO+SbWFWRSvhu8gLGv4A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@types/json-schema': 7.0.9 - '@types/semver': 7.3.12 - '@typescript-eslint/scope-manager': 5.43.0 - '@typescript-eslint/types': 5.43.0 - '@typescript-eslint/typescript-estree': 5.43.0_typescript@4.8.4 - eslint: 8.2.0 - eslint-scope: 5.1.1 - eslint-utils: 3.0.0_eslint@8.2.0 - semver: 7.3.8 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /@typescript-eslint/utils/5.43.0_z4bbprzjrhnsfa24uvmcbu7f5q: - resolution: {integrity: sha512-8nVpA6yX0sCjf7v/NDfeaOlyaIIqL7OaIGOWSPFqUKK59Gnumd3Wa+2l8oAaYO2lk0sO+SbWFWRSvhu8gLGv4A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@types/json-schema': 7.0.9 - '@types/semver': 7.3.12 - '@typescript-eslint/scope-manager': 5.43.0 - '@typescript-eslint/types': 5.43.0 - '@typescript-eslint/typescript-estree': 5.43.0_typescript@4.8.4 - eslint: 8.25.0 - eslint-scope: 5.1.1 - eslint-utils: 3.0.0_eslint@8.25.0 - semver: 7.3.8 - transitivePeerDependencies: - - supports-color - - typescript - - /@typescript-eslint/utils/5.43.0_zksrc6ykdxhogxjbhb5axiabwi: - resolution: {integrity: sha512-8nVpA6yX0sCjf7v/NDfeaOlyaIIqL7OaIGOWSPFqUKK59Gnumd3Wa+2l8oAaYO2lk0sO+SbWFWRSvhu8gLGv4A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@types/json-schema': 7.0.9 - '@types/semver': 7.3.12 - '@typescript-eslint/scope-manager': 5.43.0 - '@typescript-eslint/types': 5.43.0 - '@typescript-eslint/typescript-estree': 5.43.0_typescript@4.8.4 - eslint: 8.28.0 - eslint-scope: 5.1.1 - eslint-utils: 3.0.0_eslint@8.28.0 - semver: 7.3.8 - transitivePeerDependencies: - - supports-color - - typescript - dev: true /@typescript-eslint/visitor-keys/4.33.0: resolution: {integrity: sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==} @@ -16485,7 +16017,7 @@ packages: '@babel/runtime': 7.19.0 dev: false - /@wordpress/eslint-plugin/11.1.0_mcybzr52q3u5poieijntti3gbe: + /@wordpress/eslint-plugin/11.1.0_sxmqrsrdaatoogkbdrkrjpdxyi: resolution: {integrity: sha512-NNIgJQqibdj5BsctkhCqjYt5NAs8eGBnuMSTnbhYCuUrs/PF3iF+jwY4OEOBOSlr00KyM9BfDxr9/5EWG8PmOw==} engines: {node: '>=12', npm: '>=6.9'} peerDependencies: @@ -16500,22 +16032,22 @@ packages: optional: true dependencies: '@babel/core': 7.12.9 - '@babel/eslint-parser': 7.17.0_egbv6vphnwurm3ap2o4feqemum - '@typescript-eslint/eslint-plugin': 5.43.0_ol5heeoi7wmwb4tenztnb7jlze - '@typescript-eslint/parser': 5.43.0_zksrc6ykdxhogxjbhb5axiabwi + '@babel/eslint-parser': 7.17.0_ghetaprrho5qxr5lt7srlprvom + '@typescript-eslint/eslint-plugin': 5.43.0_7f3f6qw7x62gbtvc33ujooybme + '@typescript-eslint/parser': 5.43.0_yygwinqv3a2io74xmwofqb7uka '@wordpress/babel-preset-default': 6.17.0 '@wordpress/prettier-config': 1.1.3 cosmiconfig: 7.0.1 - eslint: 8.28.0 - eslint-config-prettier: 8.5.0_eslint@8.28.0 - eslint-plugin-import: 2.25.4_ivdjtymx6ubvknadox4oh4qsue - eslint-plugin-jest: 25.7.0_hf2laxo64q65ca427enwz6fepa - eslint-plugin-jsdoc: 37.9.7_eslint@8.28.0 - eslint-plugin-jsx-a11y: 6.5.1_eslint@8.28.0 - eslint-plugin-playwright: 0.8.0_bgyrkcoecpw7hmmqwzduoudyum - eslint-plugin-prettier: 3.4.1_qsebzixysgccx3deogol23q32e - eslint-plugin-react: 7.29.4_eslint@8.28.0 - eslint-plugin-react-hooks: 4.3.0_eslint@8.28.0 + eslint: 8.32.0 + eslint-config-prettier: 8.5.0_eslint@8.32.0 + eslint-plugin-import: 2.25.4_r7xfbnyx5thbz2q7cpgk4iifq4 + eslint-plugin-jest: 25.7.0_vrczkf54xwdqns6dzhfyfknf4i + eslint-plugin-jsdoc: 37.9.7_eslint@8.32.0 + eslint-plugin-jsx-a11y: 6.5.1_eslint@8.32.0 + eslint-plugin-playwright: 0.8.0_bue26mukq4crcytfui3g7egwhy + eslint-plugin-prettier: 3.4.1_7r3blfeknbmsg4nj2vdrt55nra + eslint-plugin-react: 7.29.4_eslint@8.32.0 + eslint-plugin-react-hooks: 4.3.0_eslint@8.32.0 globals: 13.12.0 prettier: /wp-prettier/2.6.2 requireindex: 1.2.0 @@ -16527,7 +16059,7 @@ packages: - supports-color dev: true - /@wordpress/eslint-plugin/13.3.0_fbyahr2bitmcepmkuzm2z5k2za: + /@wordpress/eslint-plugin/13.3.0_gfyzvuspv2bauiwltqdx5274hy: resolution: {integrity: sha512-90OEybXt6mJRumax5lQubPQLfv2TV3A0+cA50b3amE28bPzv76NRexbM5jeyN3Jhc0rYU4glq4yHq75XjSRVVA==} engines: {node: '>=14', npm: '>=6.14.4'} peerDependencies: @@ -16542,21 +16074,21 @@ packages: optional: true dependencies: '@babel/core': 7.17.8 - '@babel/eslint-parser': 7.17.0_2sahtlaqcjxbvw6x6d2jsqyrai - '@typescript-eslint/eslint-plugin': 5.43.0_hhpcbb6wqnhvo6wpcctutdxelq - '@typescript-eslint/parser': 5.43.0_z4bbprzjrhnsfa24uvmcbu7f5q + '@babel/eslint-parser': 7.17.0_45t77ya3ofqya5ogk4q6xdzcpq + '@typescript-eslint/eslint-plugin': 5.43.0_7f3f6qw7x62gbtvc33ujooybme + '@typescript-eslint/parser': 5.43.0_yygwinqv3a2io74xmwofqb7uka '@wordpress/babel-preset-default': 7.3.0 '@wordpress/prettier-config': 2.5.0_prettier@2.3.0 cosmiconfig: 7.0.1 - eslint: 8.25.0 - eslint-config-prettier: 8.5.0_eslint@8.25.0 - eslint-plugin-import: 2.25.4_5giipivvrkjwpspmcck55fgaja - eslint-plugin-jest: 25.7.0_3y2nwuawvch4guq6v72bjm4ajq - eslint-plugin-jsdoc: 37.9.7_eslint@8.25.0 - eslint-plugin-jsx-a11y: 6.5.1_eslint@8.25.0 - eslint-plugin-prettier: 3.4.1_xewdwxdxvhaotirttsgfltr2ce - eslint-plugin-react: 7.29.4_eslint@8.25.0 - eslint-plugin-react-hooks: 4.3.0_eslint@8.25.0 + eslint: 8.32.0 + eslint-config-prettier: 8.5.0_eslint@8.32.0 + eslint-plugin-import: 2.25.4_r7xfbnyx5thbz2q7cpgk4iifq4 + eslint-plugin-jest: 25.7.0_rwnv4yn7yzh7r363utswm6eurm + eslint-plugin-jsdoc: 37.9.7_eslint@8.32.0 + eslint-plugin-jsx-a11y: 6.5.1_eslint@8.32.0 + eslint-plugin-prettier: 3.4.1_kkndast3snpxc6vned3vep6fka + eslint-plugin-react: 7.29.4_eslint@8.32.0 + eslint-plugin-react-hooks: 4.3.0_eslint@8.32.0 globals: 13.17.0 prettier: 2.3.0 requireindex: 1.2.0 @@ -16605,7 +16137,7 @@ packages: dependencies: '@babel/eslint-parser': 7.17.0_xujkgafwcpm5gwokncqwvv5ure '@typescript-eslint/eslint-plugin': 4.33.0_k4l66av2tbo6kxzw52jzgbfzii - '@typescript-eslint/parser': 4.33.0_phzabm2hax2olcsrhm4n4aik5y + '@typescript-eslint/parser': 4.33.0_yygwinqv3a2io74xmwofqb7uka '@wordpress/prettier-config': 1.1.3 cosmiconfig: 7.0.1 eslint: 7.32.0 @@ -17401,7 +16933,7 @@ packages: - webpack-command dev: true - /@wordpress/scripts/19.2.4_acp5qrdj4cc6vmqqhp4stdanfe: + /@wordpress/scripts/19.2.4_f7x7zdz3ccrnqxb4utvdtwqz4e: resolution: {integrity: sha512-klkfjBOPfr/RT/3Tvmx+gLbZ+dxq5L0dJQHCHxEURMRW/A8SfJJPtmC29L9sE1KhO3zUMWxrkn2L6HhSzbvQbA==} engines: {node: '>=12.13', npm: '>=6.9'} hasBin: true @@ -17451,7 +16983,7 @@ packages: sass-loader: 12.6.0_sass@1.49.9+webpack@5.70.0 source-map-loader: 3.0.1_webpack@5.70.0 stylelint: 13.13.1 - terser-webpack-plugin: 5.2.5_5ksa6e7vqalagerztjs2ao5siy + terser-webpack-plugin: 5.2.5_2afcvd4rlhgtdg42ipbhcxtcri url-loader: 4.1.1_webpack@5.70.0 webpack: 5.70.0_bgqcrdgdviybk52kjcpjat65sa webpack-bundle-analyzer: 4.6.1 @@ -17837,14 +17369,6 @@ packages: dependencies: acorn: 7.4.1 - /acorn-jsx/5.3.2_acorn@8.7.0: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - acorn: 8.7.0 - dev: true - /acorn-jsx/5.3.2_acorn@8.8.1: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -18627,7 +18151,7 @@ packages: - supports-color dev: true - /babel-eslint/10.1.0_eslint@8.25.0: + /babel-eslint/10.1.0_eslint@8.32.0: resolution: {integrity: sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==} engines: {node: '>=6'} deprecated: babel-eslint is now @babel/eslint-parser. This package will no longer receive updates. @@ -18638,7 +18162,7 @@ packages: '@babel/parser': 7.17.8 '@babel/traverse': 7.17.3 '@babel/types': 7.17.0 - eslint: 8.25.0 + eslint: 8.32.0 eslint-visitor-keys: 1.3.0 resolve: 1.20.0 transitivePeerDependencies: @@ -18980,6 +18504,19 @@ packages: - 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: + '@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.3_@babel+core@7.17.8: resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} peerDependencies: @@ -22828,35 +22365,25 @@ packages: eslint: 7.32.0 dev: true - /eslint-config-prettier/8.5.0_eslint@8.25.0: + /eslint-config-prettier/8.5.0_eslint@8.32.0: resolution: {integrity: sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==} hasBin: true peerDependencies: eslint: '>=7.0.0' dependencies: - eslint: 8.25.0 - dev: false + eslint: 8.32.0 - /eslint-config-prettier/8.5.0_eslint@8.28.0: - resolution: {integrity: sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - dependencies: - eslint: 8.28.0 - dev: true - - /eslint-config-wpcalypso/5.0.0_44ie4thsizlsiqkjwba6wctao4: + /eslint-config-wpcalypso/5.0.0_3reg6c535tepz5eatsxybh5pja: resolution: {integrity: sha512-bENkOkC7Hk2LREkj9aVqv5ELqYaUZqN2IBtmCdsQXrkJBsW8FV9mOzcBHnLm3Cvw4YYfq0rZzIFuCs3pkPbe1Q==} peerDependencies: eslint: ^6.0.0 eslint-plugin-jsdoc: ^18.0.0 eslint-plugin-wpcalypso: ^3.4.1 || ^4.0.0 dependencies: - eslint: 8.25.0 - eslint-plugin-jsdoc: 18.11.0_eslint@8.25.0 - eslint-plugin-react-hooks: 2.5.1_eslint@8.25.0 - eslint-plugin-wpcalypso: 4.1.0_eslint@8.25.0 + eslint: 8.32.0 + eslint-plugin-jsdoc: 18.11.0_eslint@8.32.0 + eslint-plugin-react-hooks: 2.5.1_eslint@8.32.0 + eslint-plugin-wpcalypso: 4.1.0_eslint@8.32.0 dev: true /eslint-import-resolver-node/0.3.6: @@ -22867,7 +22394,7 @@ packages: transitivePeerDependencies: - supports-color - /eslint-import-resolver-typescript/2.5.0_7yrnqyx753fo5bwjhiag2wpedy: + /eslint-import-resolver-typescript/2.5.0_xygrkdz7akfjaillbmnvbogh5e: resolution: {integrity: sha512-qZ6e5CFr+I7K4VVhQu3M/9xGv9/YmwsEXrsm3nimw8vWaVHRDrQRp26BgCypTxBp3vUp4o5aVEJRiy0F2DFddQ==} engines: {node: '>=4'} peerDependencies: @@ -22875,8 +22402,8 @@ packages: eslint-plugin-import: '*' dependencies: debug: 4.3.3 - eslint: 8.11.0 - eslint-plugin-import: 2.25.4_mwel7v7qi7wwnb6yoytqwvyg4e + eslint: 8.32.0 + eslint-plugin-import: 2.25.4_wodm5nq5wrndbbcvxidaevzgna glob: 7.2.0 is-glob: 4.0.3 resolve: 1.20.0 @@ -22895,7 +22422,7 @@ packages: array-find: 1.0.0 debug: 3.2.7 enhanced-resolve: 0.9.1 - eslint-plugin-import: 2.25.4_mwel7v7qi7wwnb6yoytqwvyg4e + eslint-plugin-import: 2.25.4_wodm5nq5wrndbbcvxidaevzgna find-root: 1.1.0 has: 1.0.3 interpret: 1.4.0 @@ -22927,7 +22454,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.43.0_zksrc6ykdxhogxjbhb5axiabwi + '@typescript-eslint/parser': 5.43.0_yygwinqv3a2io74xmwofqb7uka debug: 3.2.7 eslint-import-resolver-node: 0.3.6 find-up: 2.1.0 @@ -22952,7 +22479,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 4.33.0_phzabm2hax2olcsrhm4n4aik5y + '@typescript-eslint/parser': 4.33.0_yygwinqv3a2io74xmwofqb7uka debug: 3.2.7 eslint-import-resolver-node: 0.3.6 find-up: 2.1.0 @@ -22978,47 +22505,16 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.43.0_himlt4eddny2rsb5zkuydvuf7u + '@typescript-eslint/parser': 5.43.0_yygwinqv3a2io74xmwofqb7uka debug: 3.2.7 eslint-import-resolver-node: 0.3.6 - eslint-import-resolver-typescript: 2.5.0_7yrnqyx753fo5bwjhiag2wpedy + eslint-import-resolver-typescript: 2.5.0_xygrkdz7akfjaillbmnvbogh5e eslint-import-resolver-webpack: 0.13.2_xlbwhpbmf5dkmuyaaidudnwply find-up: 2.1.0 transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-import/2.25.4_5giipivvrkjwpspmcck55fgaja: - resolution: {integrity: sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - dependencies: - '@typescript-eslint/parser': 5.43.0_z4bbprzjrhnsfa24uvmcbu7f5q - array-includes: 3.1.4 - array.prototype.flat: 1.2.5 - debug: 2.6.9 - doctrine: 2.1.0 - eslint: 8.25.0 - eslint-import-resolver-node: 0.3.6 - eslint-module-utils: 2.7.3_fmuy6wfytpxcy4lufnxcokvnry - has: 1.0.3 - is-core-module: 2.8.0 - is-glob: 4.0.3 - minimatch: 3.0.4 - object.values: 1.1.5 - resolve: 1.20.0 - tsconfig-paths: 3.14.0 - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - dev: false - /eslint-plugin-import/2.25.4_ffi3uiz42rv3jyhs6cr7p7qqry: resolution: {integrity: sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==} engines: {node: '>=4'} @@ -23029,7 +22525,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 4.33.0_phzabm2hax2olcsrhm4n4aik5y + '@typescript-eslint/parser': 4.33.0_yygwinqv3a2io74xmwofqb7uka array-includes: 3.1.4 array.prototype.flat: 1.2.5 debug: 2.6.9 @@ -23038,11 +22534,11 @@ packages: eslint-import-resolver-node: 0.3.6 eslint-module-utils: 2.7.3_lkzaig2qiyp6elizstfbgvzhie has: 1.0.3 - is-core-module: 2.8.0 + is-core-module: 2.10.0 is-glob: 4.0.3 - minimatch: 3.0.4 + minimatch: 3.1.2 object.values: 1.1.5 - resolve: 1.20.0 + resolve: 1.22.1 tsconfig-paths: 3.14.0 transitivePeerDependencies: - eslint-import-resolver-typescript @@ -23050,7 +22546,7 @@ packages: - supports-color dev: true - /eslint-plugin-import/2.25.4_ivdjtymx6ubvknadox4oh4qsue: + /eslint-plugin-import/2.25.4_r7xfbnyx5thbz2q7cpgk4iifq4: resolution: {integrity: sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==} engines: {node: '>=4'} peerDependencies: @@ -23060,28 +22556,27 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.43.0_zksrc6ykdxhogxjbhb5axiabwi + '@typescript-eslint/parser': 5.43.0_yygwinqv3a2io74xmwofqb7uka array-includes: 3.1.4 array.prototype.flat: 1.2.5 debug: 2.6.9 doctrine: 2.1.0 - eslint: 8.28.0 + eslint: 8.32.0 eslint-import-resolver-node: 0.3.6 eslint-module-utils: 2.7.3_fmuy6wfytpxcy4lufnxcokvnry has: 1.0.3 - is-core-module: 2.8.0 + is-core-module: 2.10.0 is-glob: 4.0.3 - minimatch: 3.0.4 + minimatch: 3.1.2 object.values: 1.1.5 - resolve: 1.20.0 + resolve: 1.22.1 tsconfig-paths: 3.14.0 transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - dev: true - /eslint-plugin-import/2.25.4_mwel7v7qi7wwnb6yoytqwvyg4e: + /eslint-plugin-import/2.25.4_wodm5nq5wrndbbcvxidaevzgna: resolution: {integrity: sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==} engines: {node: '>=4'} peerDependencies: @@ -23091,12 +22586,12 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.43.0_himlt4eddny2rsb5zkuydvuf7u + '@typescript-eslint/parser': 5.43.0_yygwinqv3a2io74xmwofqb7uka array-includes: 3.1.4 array.prototype.flat: 1.2.5 debug: 2.6.9 doctrine: 2.1.0 - eslint: 8.11.0 + eslint: 8.32.0 eslint-import-resolver-node: 0.3.6 eslint-module-utils: 2.7.3_p4g3punufuo4uj6odspnpzo2u4 has: 1.0.3 @@ -23125,27 +22620,14 @@ packages: - typescript dev: true - /eslint-plugin-jest/23.20.0_iqokrdhiz7bccawj5qurem2l4e: + /eslint-plugin-jest/23.20.0_yygwinqv3a2io74xmwofqb7uka: resolution: {integrity: sha512-+6BGQt85OREevBDWCvhqj1yYA4+BFK4XnRZSGJionuEYmcglMZYLNNBBemwzbqUAckURaHdJSBcjHPyrtypZOw==} engines: {node: '>=8'} peerDependencies: eslint: '>=5' dependencies: - '@typescript-eslint/experimental-utils': 2.34.0_iqokrdhiz7bccawj5qurem2l4e - eslint: 8.12.0 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /eslint-plugin-jest/23.20.0_z4bbprzjrhnsfa24uvmcbu7f5q: - resolution: {integrity: sha512-+6BGQt85OREevBDWCvhqj1yYA4+BFK4XnRZSGJionuEYmcglMZYLNNBBemwzbqUAckURaHdJSBcjHPyrtypZOw==} - engines: {node: '>=8'} - peerDependencies: - eslint: '>=5' - dependencies: - '@typescript-eslint/experimental-utils': 2.34.0_z4bbprzjrhnsfa24uvmcbu7f5q - eslint: 8.25.0 + '@typescript-eslint/experimental-utils': 2.34.0_yygwinqv3a2io74xmwofqb7uka + eslint: 8.32.0 transitivePeerDependencies: - supports-color - typescript @@ -23169,7 +22651,7 @@ packages: - typescript dev: true - /eslint-plugin-jest/25.7.0_3y2nwuawvch4guq6v72bjm4ajq: + /eslint-plugin-jest/25.7.0_rwnv4yn7yzh7r363utswm6eurm: resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} peerDependencies: @@ -23182,16 +22664,16 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.43.0_hhpcbb6wqnhvo6wpcctutdxelq - '@typescript-eslint/experimental-utils': 5.43.0_z4bbprzjrhnsfa24uvmcbu7f5q - eslint: 8.25.0 + '@typescript-eslint/eslint-plugin': 5.43.0_7f3f6qw7x62gbtvc33ujooybme + '@typescript-eslint/experimental-utils': 5.43.0_yygwinqv3a2io74xmwofqb7uka + eslint: 8.32.0 jest: 27.5.1 transitivePeerDependencies: - supports-color - typescript dev: false - /eslint-plugin-jest/25.7.0_hf2laxo64q65ca427enwz6fepa: + /eslint-plugin-jest/25.7.0_vrczkf54xwdqns6dzhfyfknf4i: resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} peerDependencies: @@ -23204,16 +22686,16 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.43.0_ol5heeoi7wmwb4tenztnb7jlze - '@typescript-eslint/experimental-utils': 5.43.0_zksrc6ykdxhogxjbhb5axiabwi - eslint: 8.28.0 + '@typescript-eslint/eslint-plugin': 5.43.0_7f3f6qw7x62gbtvc33ujooybme + '@typescript-eslint/experimental-utils': 5.43.0_yygwinqv3a2io74xmwofqb7uka + eslint: 8.32.0 jest: 27.3.1 transitivePeerDependencies: - supports-color - typescript dev: true - /eslint-plugin-jsdoc/18.11.0_eslint@8.25.0: + /eslint-plugin-jsdoc/18.11.0_eslint@8.32.0: resolution: {integrity: sha512-24J2+eK2ZHZ1KvpKcoOEir2k4xJKfPzZ1JC9PToi8y8Tn59T8TVVSNRTTRzsDdiaQeIbehApB3KxqIfJG8o7hg==} engines: {node: '>=6'} peerDependencies: @@ -23221,7 +22703,7 @@ packages: dependencies: comment-parser: 0.7.6 debug: 4.3.4 - eslint: 8.25.0 + eslint: 8.32.0 jsdoctypeparser: 6.1.0 lodash: 4.17.21 object.entries-ponyfill: 1.0.1 @@ -23270,7 +22752,7 @@ packages: - supports-color dev: true - /eslint-plugin-jsdoc/37.9.7_eslint@8.25.0: + /eslint-plugin-jsdoc/37.9.7_eslint@8.32.0: resolution: {integrity: sha512-8alON8yYcStY94o0HycU2zkLKQdcS+qhhOUNQpfONHHwvI99afbmfpYuPqf6PbLz5pLZldG3Te5I0RbAiTN42g==} engines: {node: ^12 || ^14 || ^16 || ^17} peerDependencies: @@ -23280,33 +22762,13 @@ packages: comment-parser: 1.3.0 debug: 4.3.4 escape-string-regexp: 4.0.0 - eslint: 8.25.0 + eslint: 8.32.0 esquery: 1.4.0 regextras: 0.8.0 semver: 7.3.8 spdx-expression-parse: 3.0.1 transitivePeerDependencies: - supports-color - dev: false - - /eslint-plugin-jsdoc/37.9.7_eslint@8.28.0: - resolution: {integrity: sha512-8alON8yYcStY94o0HycU2zkLKQdcS+qhhOUNQpfONHHwvI99afbmfpYuPqf6PbLz5pLZldG3Te5I0RbAiTN42g==} - engines: {node: ^12 || ^14 || ^16 || ^17} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - dependencies: - '@es-joy/jsdoccomment': 0.20.1 - comment-parser: 1.3.0 - debug: 4.3.4 - escape-string-regexp: 4.0.0 - eslint: 8.28.0 - esquery: 1.4.0 - regextras: 0.8.0 - semver: 7.3.8 - spdx-expression-parse: 3.0.1 - transitivePeerDependencies: - - supports-color - dev: true /eslint-plugin-jsx-a11y/6.5.1_eslint@7.32.0: resolution: {integrity: sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g==} @@ -23329,7 +22791,7 @@ packages: minimatch: 3.1.2 dev: true - /eslint-plugin-jsx-a11y/6.5.1_eslint@8.25.0: + /eslint-plugin-jsx-a11y/6.5.1_eslint@8.32.0: resolution: {integrity: sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g==} engines: {node: '>=4.0'} peerDependencies: @@ -23343,33 +22805,11 @@ packages: axobject-query: 2.2.0 damerau-levenshtein: 1.0.7 emoji-regex: 9.2.2 - eslint: 8.25.0 + eslint: 8.32.0 has: 1.0.3 jsx-ast-utils: 3.2.1 language-tags: 1.0.5 minimatch: 3.1.2 - dev: false - - /eslint-plugin-jsx-a11y/6.5.1_eslint@8.28.0: - resolution: {integrity: sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g==} - engines: {node: '>=4.0'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - dependencies: - '@babel/runtime': 7.19.0 - aria-query: 4.2.2 - array-includes: 3.1.4 - ast-types-flow: 0.0.7 - axe-core: 4.3.5 - axobject-query: 2.2.0 - damerau-levenshtein: 1.0.7 - emoji-regex: 9.2.2 - eslint: 8.28.0 - has: 1.0.3 - jsx-ast-utils: 3.2.1 - language-tags: 1.0.5 - minimatch: 3.1.2 - dev: true /eslint-plugin-markdown/1.0.2: resolution: {integrity: sha512-BfvXKsO0K+zvdarNc801jsE/NTLmig4oKhZ1U3aSUgTf2dB/US5+CrfGxMsCK2Ki1vS1R3HPok+uYpufFndhzw==} @@ -23392,7 +22832,7 @@ packages: - supports-color dev: true - /eslint-plugin-playwright/0.8.0_bgyrkcoecpw7hmmqwzduoudyum: + /eslint-plugin-playwright/0.8.0_bue26mukq4crcytfui3g7egwhy: resolution: {integrity: sha512-9uJH25m6H3jwU5O7bHD5M8cLx46L72EnIUe3dZqTox6M+WzOFzeUWaDJHHCdLGXZ8XlAU4mbCZnP7uhjKepfRA==} peerDependencies: eslint: '>=7' @@ -23401,8 +22841,25 @@ packages: eslint-plugin-jest: optional: true dependencies: - eslint: 8.28.0 - eslint-plugin-jest: 25.7.0_hf2laxo64q65ca427enwz6fepa + eslint: 8.32.0 + eslint-plugin-jest: 25.7.0_vrczkf54xwdqns6dzhfyfknf4i + dev: true + + /eslint-plugin-prettier/3.4.1_7r3blfeknbmsg4nj2vdrt55nra: + resolution: {integrity: sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==} + engines: {node: '>=6.0.0'} + peerDependencies: + eslint: '>=5.0.0' + eslint-config-prettier: '*' + prettier: '>=1.13.0' + peerDependenciesMeta: + eslint-config-prettier: + optional: true + dependencies: + eslint: 8.32.0 + eslint-config-prettier: 8.5.0_eslint@8.32.0 + prettier: /wp-prettier/2.6.2 + prettier-linter-helpers: 1.0.0 dev: true /eslint-plugin-prettier/3.4.1_gs3qp45fhmeuf44rtqp7mvwogy: @@ -23422,7 +22879,7 @@ packages: prettier-linter-helpers: 1.0.0 dev: true - /eslint-plugin-prettier/3.4.1_qsebzixysgccx3deogol23q32e: + /eslint-plugin-prettier/3.4.1_kkndast3snpxc6vned3vep6fka: resolution: {integrity: sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==} engines: {node: '>=6.0.0'} peerDependencies: @@ -23433,25 +22890,8 @@ packages: eslint-config-prettier: optional: true dependencies: - eslint: 8.28.0 - eslint-config-prettier: 8.5.0_eslint@8.28.0 - prettier: /wp-prettier/2.6.2 - prettier-linter-helpers: 1.0.0 - dev: true - - /eslint-plugin-prettier/3.4.1_xewdwxdxvhaotirttsgfltr2ce: - resolution: {integrity: sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==} - engines: {node: '>=6.0.0'} - peerDependencies: - eslint: '>=5.0.0' - eslint-config-prettier: '*' - prettier: '>=1.13.0' - peerDependenciesMeta: - eslint-config-prettier: - optional: true - dependencies: - eslint: 8.25.0 - eslint-config-prettier: 8.5.0_eslint@8.25.0 + eslint: 8.32.0 + eslint-config-prettier: 8.5.0_eslint@8.32.0 prettier: 2.3.0 prettier-linter-helpers: 1.0.0 dev: false @@ -23473,13 +22913,13 @@ packages: prettier-linter-helpers: 1.0.0 dev: true - /eslint-plugin-react-hooks/2.5.1_eslint@8.25.0: + /eslint-plugin-react-hooks/2.5.1_eslint@8.32.0: resolution: {integrity: sha512-Y2c4b55R+6ZzwtTppKwSmK/Kar8AdLiC2f9NADCuxbcTgPPg41Gyqa6b9GppgXSvCtkRw43ZE86CT5sejKC6/g==} engines: {node: '>=7'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 dependencies: - eslint: 8.25.0 + eslint: 8.32.0 dev: true /eslint-plugin-react-hooks/4.3.0_eslint@7.32.0: @@ -23491,23 +22931,13 @@ packages: eslint: 7.32.0 dev: true - /eslint-plugin-react-hooks/4.3.0_eslint@8.25.0: + /eslint-plugin-react-hooks/4.3.0_eslint@8.32.0: resolution: {integrity: sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==} engines: {node: '>=10'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 dependencies: - eslint: 8.25.0 - dev: false - - /eslint-plugin-react-hooks/4.3.0_eslint@8.28.0: - resolution: {integrity: sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==} - engines: {node: '>=10'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 - dependencies: - eslint: 8.28.0 - dev: true + eslint: 8.32.0 /eslint-plugin-react/7.29.4_eslint@7.32.0: resolution: {integrity: sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ==} @@ -23532,7 +22962,7 @@ packages: string.prototype.matchall: 4.0.6 dev: true - /eslint-plugin-react/7.29.4_eslint@8.11.0: + /eslint-plugin-react/7.29.4_eslint@8.32.0: resolution: {integrity: sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ==} engines: {node: '>=4'} peerDependencies: @@ -23541,7 +22971,7 @@ packages: array-includes: 3.1.4 array.prototype.flatmap: 1.2.5 doctrine: 2.1.0 - eslint: 8.11.0 + eslint: 8.32.0 estraverse: 5.3.0 jsx-ast-utils: 3.2.1 minimatch: 3.1.2 @@ -23553,74 +22983,27 @@ packages: resolve: 2.0.0-next.3 semver: 6.3.0 string.prototype.matchall: 4.0.6 - dev: true - /eslint-plugin-react/7.29.4_eslint@8.25.0: - resolution: {integrity: sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - dependencies: - array-includes: 3.1.4 - array.prototype.flatmap: 1.2.5 - doctrine: 2.1.0 - eslint: 8.25.0 - estraverse: 5.3.0 - jsx-ast-utils: 3.2.1 - minimatch: 3.1.2 - object.entries: 1.1.5 - object.fromentries: 2.0.5 - object.hasown: 1.1.0 - object.values: 1.1.5 - prop-types: 15.8.1 - resolve: 2.0.0-next.3 - semver: 6.3.0 - string.prototype.matchall: 4.0.6 - dev: false - - /eslint-plugin-react/7.29.4_eslint@8.28.0: - resolution: {integrity: sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - dependencies: - array-includes: 3.1.4 - array.prototype.flatmap: 1.2.5 - doctrine: 2.1.0 - eslint: 8.28.0 - estraverse: 5.3.0 - jsx-ast-utils: 3.2.1 - minimatch: 3.1.2 - object.entries: 1.1.5 - object.fromentries: 2.0.5 - object.hasown: 1.1.0 - object.values: 1.1.5 - prop-types: 15.8.1 - resolve: 2.0.0-next.3 - semver: 6.3.0 - string.prototype.matchall: 4.0.6 - dev: true - - /eslint-plugin-testing-library/5.1.0_z4bbprzjrhnsfa24uvmcbu7f5q: + /eslint-plugin-testing-library/5.1.0_yygwinqv3a2io74xmwofqb7uka: resolution: {integrity: sha512-YSNzasJUbyhOTe14ZPygeOBvcPvcaNkwHwrj4vdf+uirr2D32JTDaKi6CP5Os2aWtOcvt4uBSPXp9h5xGoqvWQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} peerDependencies: eslint: ^7.5.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.15.0_z4bbprzjrhnsfa24uvmcbu7f5q - eslint: 8.25.0 + '@typescript-eslint/utils': 5.15.0_yygwinqv3a2io74xmwofqb7uka + eslint: 8.32.0 transitivePeerDependencies: - supports-color - typescript dev: false - /eslint-plugin-wpcalypso/4.1.0_eslint@8.25.0: + /eslint-plugin-wpcalypso/4.1.0_eslint@8.32.0: resolution: {integrity: sha512-2gZdaaX5rS7vve5FfIBTANPFXfQstxMppUFR8KzrY5cJPt7hIhpg9lOb4y0hVYNdJkhZxkvEWw8yoJhaUc1OYQ==} engines: {node: '>=10'} peerDependencies: eslint: '>=5' dependencies: - eslint: 8.25.0 + eslint: 8.32.0 dev: true /eslint-scope/4.0.3: @@ -23638,14 +23021,6 @@ packages: esrecurse: 4.3.0 estraverse: 4.3.0 - /eslint-scope/6.0.0: - resolution: {integrity: sha512-uRDL9MWmQCkaFus8RF5K9/L/2fn+80yoW3jkD53l4shjCh26fCtvJGasxjUqP5OT87SYTxCVA3BwTUzuELx9kA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - dev: true - /eslint-scope/7.1.1: resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -23676,52 +23051,13 @@ packages: eslint-visitor-keys: 2.1.0 dev: true - /eslint-utils/3.0.0_eslint@8.11.0: + /eslint-utils/3.0.0_eslint@8.32.0: resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} peerDependencies: eslint: '>=5' dependencies: - eslint: 8.11.0 - eslint-visitor-keys: 2.1.0 - dev: true - - /eslint-utils/3.0.0_eslint@8.12.0: - resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} - engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} - peerDependencies: - eslint: '>=5' - dependencies: - eslint: 8.12.0 - eslint-visitor-keys: 2.1.0 - dev: true - - /eslint-utils/3.0.0_eslint@8.2.0: - resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} - engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} - peerDependencies: - eslint: '>=5' - dependencies: - eslint: 8.2.0 - eslint-visitor-keys: 2.1.0 - dev: true - - /eslint-utils/3.0.0_eslint@8.25.0: - resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} - engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} - peerDependencies: - eslint: '>=5' - dependencies: - eslint: 8.25.0 - eslint-visitor-keys: 2.1.0 - - /eslint-utils/3.0.0_eslint@8.28.0: - resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} - engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} - peerDependencies: - eslint: '>=5' - dependencies: - eslint: 8.28.0 + eslint: 8.32.0 eslint-visitor-keys: 2.1.0 /eslint-visitor-keys/1.3.0: @@ -23732,11 +23068,6 @@ packages: resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} engines: {node: '>=10'} - /eslint-visitor-keys/3.0.0: - resolution: {integrity: sha512-mJOZa35trBTb3IyRmo8xmKBZlxf+N7OnUl4+ZhJHs/r+0770Wh/LEACE2pqMGMe27G/4y8P2bYGk4J70IC5k1Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - /eslint-visitor-keys/3.3.0: resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -23746,7 +23077,7 @@ packages: engines: {node: ^6.14.0 || ^8.10.0 || >=9.10.0} hasBin: true dependencies: - '@babel/code-frame': 7.16.7 + '@babel/code-frame': 7.18.6 ajv: 6.12.6 chalk: 2.4.2 cross-spawn: 6.0.5 @@ -23857,7 +23188,7 @@ packages: file-entry-cache: 6.0.1 functional-red-black-tree: 1.0.1 glob-parent: 5.1.2 - globals: 13.18.0 + globals: 13.19.0 ignore: 4.0.6 import-fresh: 3.3.0 imurmurhash: 0.1.4 @@ -23881,194 +23212,13 @@ packages: - supports-color dev: true - /eslint/8.11.0: - resolution: {integrity: sha512-/KRpd9mIRg2raGxHRGwW9ZywYNAClZrHjdueHcrVDuO3a6bj83eoTirCCk0M0yPwOjWYKHwRVRid+xK4F/GHgA==} + /eslint/8.32.0: + resolution: {integrity: sha512-nETVXpnthqKPFyuY2FNjz/bEd6nbosRgKbkgS/y1C7LJop96gYHWpiguLecMHQ2XCPxn77DS0P+68WzG6vkZSQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint/eslintrc': 1.2.1 - '@humanwhocodes/config-array': 0.9.5 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.3 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.1.1 - eslint-utils: 3.0.0_eslint@8.11.0 - eslint-visitor-keys: 3.3.0 - espree: 9.3.1 - esquery: 1.4.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - functional-red-black-tree: 1.0.1 - glob-parent: 6.0.2 - globals: 13.12.0 - ignore: 5.2.0 - import-fresh: 3.3.0 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.1 - regexpp: 3.2.0 - strip-ansi: 6.0.1 - strip-json-comments: 3.1.1 - text-table: 0.2.0 - v8-compile-cache: 2.3.0 - transitivePeerDependencies: - - supports-color - dev: true - - /eslint/8.12.0: - resolution: {integrity: sha512-it1oBL9alZg1S8UycLm5YDMAkIhtH6FtAzuZs6YvoGVldWjbS08BkAdb/ymP9LlAyq8koANu32U7Ib/w+UNh8Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true - dependencies: - '@eslint/eslintrc': 1.2.1 - '@humanwhocodes/config-array': 0.9.5 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.1.1 - eslint-utils: 3.0.0_eslint@8.12.0 - eslint-visitor-keys: 3.3.0 - espree: 9.3.1 - esquery: 1.4.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - functional-red-black-tree: 1.0.1 - glob-parent: 6.0.2 - globals: 13.12.0 - ignore: 5.2.0 - import-fresh: 3.3.0 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.1 - regexpp: 3.2.0 - strip-ansi: 6.0.1 - strip-json-comments: 3.1.1 - text-table: 0.2.0 - v8-compile-cache: 2.3.0 - transitivePeerDependencies: - - supports-color - dev: true - - /eslint/8.2.0: - resolution: {integrity: sha512-erw7XmM+CLxTOickrimJ1SiF55jiNlVSp2qqm0NuBWPtHYQCegD5ZMaW0c3i5ytPqL+SSLaCxdvQXFPLJn+ABw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true - dependencies: - '@eslint/eslintrc': 1.0.4 - '@humanwhocodes/config-array': 0.6.0 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.2 - doctrine: 3.0.0 - enquirer: 2.3.6 - escape-string-regexp: 4.0.0 - eslint-scope: 6.0.0 - eslint-utils: 3.0.0_eslint@8.2.0 - eslint-visitor-keys: 3.0.0 - espree: 9.0.0 - esquery: 1.4.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - functional-red-black-tree: 1.0.1 - glob-parent: 6.0.2 - globals: 13.12.0 - ignore: 4.0.6 - import-fresh: 3.3.0 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.0.4 - natural-compare: 1.4.0 - optionator: 0.9.1 - progress: 2.0.3 - regexpp: 3.2.0 - semver: 7.3.5 - strip-ansi: 6.0.1 - strip-json-comments: 3.1.1 - text-table: 0.2.0 - v8-compile-cache: 2.3.0 - transitivePeerDependencies: - - supports-color - dev: true - - /eslint/8.25.0: - resolution: {integrity: sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true - dependencies: - '@eslint/eslintrc': 1.3.3 - '@humanwhocodes/config-array': 0.10.7 - '@humanwhocodes/module-importer': 1.0.1 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.1.1 - eslint-utils: 3.0.0_eslint@8.25.0 - eslint-visitor-keys: 3.3.0 - espree: 9.4.0 - esquery: 1.4.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.17.0 - globby: 11.1.0 - grapheme-splitter: 1.0.4 - ignore: 5.2.0 - import-fresh: 3.3.0 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - js-sdsl: 4.1.5 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.1 - regexpp: 3.2.0 - strip-ansi: 6.0.1 - strip-json-comments: 3.1.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - - /eslint/8.28.0: - resolution: {integrity: sha512-S27Di+EVyMxcHiwDrFzk8dJYAaD+/5SoWKxL1ri/71CRHsnJnRDPNt2Kzj24+MT9FDupf4aqqyqPrvI8MvQ4VQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true - dependencies: - '@eslint/eslintrc': 1.3.3 - '@humanwhocodes/config-array': 0.11.7 + '@eslint/eslintrc': 1.4.1 + '@humanwhocodes/config-array': 0.11.8 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 ajv: 6.12.6 @@ -24078,7 +23228,7 @@ packages: doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.1.1 - eslint-utils: 3.0.0_eslint@8.28.0 + eslint-utils: 3.0.0_eslint@8.32.0 eslint-visitor-keys: 3.3.0 espree: 9.4.1 esquery: 1.4.0 @@ -24087,7 +23237,7 @@ packages: file-entry-cache: 6.0.1 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.18.0 + globals: 13.19.0 grapheme-splitter: 1.0.4 ignore: 5.2.0 import-fresh: 3.3.0 @@ -24136,32 +23286,6 @@ packages: eslint-visitor-keys: 1.3.0 dev: true - /espree/9.0.0: - resolution: {integrity: sha512-r5EQJcYZ2oaGbeR0jR0fFVijGOcwai07/690YRXLINuhmVeRY4UKSAsQPe/0BNuDgwP7Ophoc1PRsr2E3tkbdQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - acorn: 8.7.0 - acorn-jsx: 5.3.2_acorn@8.7.0 - eslint-visitor-keys: 3.3.0 - dev: true - - /espree/9.3.1: - resolution: {integrity: sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - acorn: 8.7.0 - acorn-jsx: 5.3.2_acorn@8.7.0 - eslint-visitor-keys: 3.3.0 - dev: true - - /espree/9.4.0: - resolution: {integrity: sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - acorn: 8.8.1 - acorn-jsx: 5.3.2_acorn@8.8.1 - eslint-visitor-keys: 3.3.0 - /espree/9.4.1: resolution: {integrity: sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -25111,7 +24235,7 @@ packages: /forever-agent/0.6.1: resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} - /fork-ts-checker-webpack-plugin/4.1.6_hsuy55rzeh5wgjgxknlp6ffswa: + /fork-ts-checker-webpack-plugin/4.1.6_62fz4gragfs3w5a4iegi53ru5i: resolution: {integrity: sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw==} engines: {node: '>=6.11.5', yarn: '>=1.0.0'} peerDependencies: @@ -25127,7 +24251,7 @@ packages: dependencies: '@babel/code-frame': 7.18.6 chalk: 2.4.2 - eslint: 8.12.0 + eslint: 8.32.0 micromatch: 3.1.10 minimatch: 3.1.2 semver: 5.7.1 @@ -25180,7 +24304,7 @@ packages: vue-template-compiler: optional: true dependencies: - '@babel/code-frame': 7.16.7 + '@babel/code-frame': 7.18.6 '@types/json-schema': 7.0.9 chalk: 4.1.2 chokidar: 3.5.3 @@ -25191,13 +24315,13 @@ packages: memfs: 3.3.0 minimatch: 3.1.2 schema-utils: 2.7.0 - semver: 7.3.5 + semver: 7.3.8 tapable: 1.1.3 typescript: 4.8.4 webpack: 5.70.0 dev: true - /fork-ts-checker-webpack-plugin/6.5.0_hsuy55rzeh5wgjgxknlp6ffswa: + /fork-ts-checker-webpack-plugin/6.5.0_5spy6wmzwqixc3k64gvf5wblh4: resolution: {integrity: sha512-cS178Y+xxtIjEUorcHddKS7yCMlrDPV31mt47blKKRfMd70Kxu5xruAFE2o9sDY6wVC5deuob/u/alD04YYHnw==} engines: {node: '>=10', yarn: '>=1.0.0'} peerDependencies: @@ -25217,39 +24341,7 @@ packages: chokidar: 3.5.3 cosmiconfig: 6.0.0 deepmerge: 4.2.2 - eslint: 8.12.0 - fs-extra: 9.1.0 - glob: 7.2.0 - memfs: 3.3.0 - minimatch: 3.1.2 - schema-utils: 2.7.0 - semver: 7.3.5 - tapable: 1.1.3 - typescript: 4.8.4 - webpack: 4.46.0_webpack-cli@3.3.12 - dev: true - - /fork-ts-checker-webpack-plugin/6.5.0_k7hdfuiil3x6lfvxi3pndxxjmi: - resolution: {integrity: sha512-cS178Y+xxtIjEUorcHddKS7yCMlrDPV31mt47blKKRfMd70Kxu5xruAFE2o9sDY6wVC5deuob/u/alD04YYHnw==} - engines: {node: '>=10', yarn: '>=1.0.0'} - peerDependencies: - eslint: '>= 6' - typescript: '>= 2.7' - vue-template-compiler: '*' - webpack: '>= 4' - peerDependenciesMeta: - eslint: - optional: true - vue-template-compiler: - optional: true - dependencies: - '@babel/code-frame': 7.16.7 - '@types/json-schema': 7.0.9 - chalk: 4.1.2 - chokidar: 3.5.3 - cosmiconfig: 6.0.0 - deepmerge: 4.2.2 - eslint: 8.11.0 + eslint: 8.32.0 fs-extra: 9.1.0 glob: 7.2.0 memfs: 3.3.0 @@ -25261,6 +24353,38 @@ packages: webpack: 5.70.0_webpack-cli@4.9.2 dev: true + /fork-ts-checker-webpack-plugin/6.5.0_62fz4gragfs3w5a4iegi53ru5i: + resolution: {integrity: sha512-cS178Y+xxtIjEUorcHddKS7yCMlrDPV31mt47blKKRfMd70Kxu5xruAFE2o9sDY6wVC5deuob/u/alD04YYHnw==} + engines: {node: '>=10', yarn: '>=1.0.0'} + peerDependencies: + eslint: '>= 6' + typescript: '>= 2.7' + vue-template-compiler: '*' + webpack: '>= 4' + peerDependenciesMeta: + eslint: + optional: true + vue-template-compiler: + optional: true + dependencies: + '@babel/code-frame': 7.18.6 + '@types/json-schema': 7.0.9 + chalk: 4.1.2 + chokidar: 3.5.3 + cosmiconfig: 6.0.0 + deepmerge: 4.2.2 + eslint: 8.32.0 + fs-extra: 9.1.0 + glob: 7.2.0 + memfs: 3.3.0 + minimatch: 3.1.2 + schema-utils: 2.7.0 + semver: 7.3.8 + tapable: 1.1.3 + typescript: 4.8.4 + webpack: 4.46.0_webpack-cli@3.3.12 + dev: true + /fork-ts-checker-webpack-plugin/6.5.0_lasgyenclx45ngbljrbo537mpe: resolution: {integrity: sha512-cS178Y+xxtIjEUorcHddKS7yCMlrDPV31mt47blKKRfMd70Kxu5xruAFE2o9sDY6wVC5deuob/u/alD04YYHnw==} engines: {node: '>=10', yarn: '>=1.0.0'} @@ -25275,7 +24399,7 @@ packages: vue-template-compiler: optional: true dependencies: - '@babel/code-frame': 7.16.7 + '@babel/code-frame': 7.18.6 '@types/json-schema': 7.0.9 chalk: 4.1.2 chokidar: 3.5.3 @@ -25286,7 +24410,7 @@ packages: memfs: 3.3.0 minimatch: 3.1.2 schema-utils: 2.7.0 - semver: 7.3.5 + semver: 7.3.8 tapable: 1.1.3 typescript: 4.8.4 webpack: 4.46.0 @@ -25553,7 +24677,7 @@ packages: functions-have-names: 1.2.2 /functional-red-black-tree/1.0.1: - resolution: {integrity: sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=} + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} /functions-have-names/1.2.2: resolution: {integrity: sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA==} @@ -25902,9 +25026,10 @@ packages: engines: {node: '>=8'} dependencies: type-fest: 0.20.2 + dev: false - /globals/13.18.0: - resolution: {integrity: sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==} + /globals/13.19.0: + resolution: {integrity: sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==} engines: {node: '>=8'} dependencies: type-fest: 0.20.2 @@ -29945,9 +29070,6 @@ packages: /js-base64/2.6.4: resolution: {integrity: sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==} - /js-sdsl/4.1.5: - resolution: {integrity: sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==} - /js-sdsl/4.2.0: resolution: {integrity: sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==} @@ -31911,6 +31033,7 @@ 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==} @@ -38900,33 +38023,6 @@ packages: - acorn dev: true - /terser-webpack-plugin/5.2.5_5ksa6e7vqalagerztjs2ao5siy: - resolution: {integrity: sha512-3luOVHku5l0QBeYS8r4CdHYWEGMmIj3H1U64jgkdZzECcSOJAyJ9TjuqcQZvw1Y+4AOBN9SeYJPJmFn2cM4/2g==} - engines: {node: '>= 10.13.0'} - peerDependencies: - '@swc/core': '*' - esbuild: '*' - uglify-js: '*' - webpack: ^5.1.0 - peerDependenciesMeta: - '@swc/core': - optional: true - esbuild: - optional: true - uglify-js: - optional: true - dependencies: - jest-worker: 27.5.1 - schema-utils: 3.1.1 - serialize-javascript: 6.0.0 - source-map: 0.6.1 - terser: 5.10.0_acorn@6.4.2 - uglify-js: 3.14.5 - webpack: 5.70.0_bgqcrdgdviybk52kjcpjat65sa - transitivePeerDependencies: - - acorn - dev: true - /terser-webpack-plugin/5.2.5_acorn@8.8.1+webpack@5.70.0: resolution: {integrity: sha512-3luOVHku5l0QBeYS8r4CdHYWEGMmIj3H1U64jgkdZzECcSOJAyJ9TjuqcQZvw1Y+4AOBN9SeYJPJmFn2cM4/2g==} engines: {node: '>= 10.13.0'} @@ -38963,22 +38059,6 @@ packages: source-map-support: 0.5.20 dev: true - /terser/5.10.0_acorn@6.4.2: - resolution: {integrity: sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==} - engines: {node: '>=10'} - hasBin: true - peerDependencies: - acorn: ^8.5.0 - peerDependenciesMeta: - acorn: - optional: true - dependencies: - acorn: 6.4.2 - commander: 2.20.3 - source-map: 0.7.3 - source-map-support: 0.5.20 - dev: true - /terser/5.10.0_acorn@7.4.1: resolution: {integrity: sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==} engines: {node: '>=10'} diff --git a/tools/code-analyzer/package.json b/tools/code-analyzer/package.json index 1781c296fc8..2ad5d14f7be 100644 --- a/tools/code-analyzer/package.json +++ b/tools/code-analyzer/package.json @@ -20,7 +20,7 @@ "devDependencies": { "@types/node": "^16.9.4", "@woocommerce/eslint-plugin": "workspace:*", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "ts-node": "^10.2.1", "tslib": "^2.3.1", "typescript": "^4.8.3" diff --git a/tools/monorepo-merge/package.json b/tools/monorepo-merge/package.json index ce3c700d37e..525d639ddbf 100644 --- a/tools/monorepo-merge/package.json +++ b/tools/monorepo-merge/package.json @@ -26,7 +26,7 @@ "@octokit/request-error": "^3.0.1", "@woocommerce/eslint-plugin": "workspace:*", "@types/node": "^16.9.4", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "globby": "^11", "jscodeshift": "^0.13.1", "oclif": "^2", From 7bb042fcd22affc206acfb51575b8c12dcf99a12 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Thu, 2 Feb 2023 21:50:36 +0800 Subject: [PATCH 141/228] 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 5d0c82f5c3982b6cf209a46f4af180387ece7964 Mon Sep 17 00:00:00 2001 From: Mahdi Taleghani Date: Thu, 2 Feb 2023 22:28:49 +0330 Subject: [PATCH 142/228] Change order of checkout address fields for Iran (#36491) * add Iran country to get_country_locale * create changelog file * fix Code sniff convention for spacing --- .../changelog/add-default_address_fields_for_Iran | 4 ++++ .../woocommerce/includes/class-wc-countries.php | 14 ++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-default_address_fields_for_Iran diff --git a/plugins/woocommerce/changelog/add-default_address_fields_for_Iran b/plugins/woocommerce/changelog/add-default_address_fields_for_Iran new file mode 100644 index 00000000000..e3d19aec06f --- /dev/null +++ b/plugins/woocommerce/changelog/add-default_address_fields_for_Iran @@ -0,0 +1,4 @@ +Significance: minor +Type: tweak + +Add IR and fields priorities to list of get_country_locale() method to follow conventional way of addressing in Iran. diff --git a/plugins/woocommerce/includes/class-wc-countries.php b/plugins/woocommerce/includes/class-wc-countries.php index bfaf83a6b06..c88e63b55c1 100644 --- a/plugins/woocommerce/includes/class-wc-countries.php +++ b/plugins/woocommerce/includes/class-wc-countries.php @@ -1164,6 +1164,20 @@ class WC_Countries { 'label' => __( 'State', 'woocommerce' ), ), ), + 'IR' => array( + 'state' => array( + 'priority' => 50, + ), + 'city' => array( + 'priority' => 60, + ), + 'address_1' => array( + 'priority' => 70, + ), + 'address_2' => array( + 'priority' => 80, + ), + ), 'IT' => array( 'postcode' => array( 'priority' => 65, From 89438cf873d058024d5d04038dd0c3afa9948b60 Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Thu, 2 Feb 2023 13:04:32 -0800 Subject: [PATCH 143/228] Remove variations logic from the AttributeControl component (#36563) * Move attribute data changes out of attribute control * Move dynamic text to props * Add onModalOpen callback * Add onModal close prop * Add changelog entry * Use closeModal for edit attribute modal * Add onEdit callback * Extract util to get unique attribute id * Rename editingAttribute to currentAttribute * Rename showAttributeModal to isNewModalVisible * Rename AddAttributeModal to NewAttributeModal * Fix up handle add attributes * Add onRemove callback for attibutes * Fix up modal styling classes * Fix up closing edit modal * Fix up modal title for options * Handle PR feedback * Separate handlers for edit and new modal callbacks * Update event names to be option/attribute specific * Use default callback functions * Handle PR feedback --- .../attribute-control/attribute-control.tsx | 259 +++++++++--------- .../edit-attribute-modal.scss | 8 + .../edit-attribute-modal.tsx | 2 +- ...te-modal.scss => new-attribute-modal.scss} | 2 +- ...bute-modal.tsx => new-attribute-modal.tsx} | 33 ++- .../test/attribute-field.spec.tsx | 1 - ....spec.tsx => new-attribute-modal.spec.tsx} | 20 +- .../fields/attribute-control/utils.ts | 9 + .../add-attribute-list-item.tsx | 11 +- .../products/fields/attributes/attributes.tsx | 27 +- .../products/fields/options/options.tsx | 33 ++- .../products/hooks/use-product-attributes.ts | 13 +- plugins/woocommerce/changelog/update-36515 | 4 + 13 files changed, 248 insertions(+), 174 deletions(-) rename plugins/woocommerce-admin/client/products/fields/attribute-control/{add-attribute-modal.scss => new-attribute-modal.scss} (97%) rename plugins/woocommerce-admin/client/products/fields/attribute-control/{add-attribute-modal.tsx => new-attribute-modal.tsx} (91%) rename plugins/woocommerce-admin/client/products/fields/attribute-control/test/{add-attribute-modal.spec.tsx => new-attribute-modal.spec.tsx} (96%) create mode 100644 plugins/woocommerce/changelog/update-36515 diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-control/attribute-control.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-control/attribute-control.tsx index 01e03c2f89b..19df9cd295d 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-control/attribute-control.tsx +++ b/plugins/woocommerce-admin/client/products/fields/attribute-control/attribute-control.tsx @@ -9,7 +9,6 @@ import { __experimentalSelectControlMenuSlot as SelectControlMenuSlot, Link, } from '@woocommerce/components'; -import { recordEvent } from '@woocommerce/tracks'; import interpolateComponents from '@automattic/interpolate-components'; import { getAdminLink } from '@woocommerce/settings'; @@ -17,48 +16,69 @@ import { getAdminLink } from '@woocommerce/settings'; * Internal dependencies */ import './attribute-field.scss'; -import { AddAttributeModal } from './add-attribute-modal'; import { EditAttributeModal } from './edit-attribute-modal'; import { EnhancedProductAttribute } from '~/products/hooks/use-product-attributes'; import { + getAttributeId, getAttributeKey, reorderSortableProductAttributePositions, } from './utils'; import { AttributeEmptyState } from '../attribute-empty-state'; import { - AddAttributeListItem, AttributeListItem, + NewAttributeListItem, } from '../attribute-list-item'; +import { NewAttributeModal } from './new-attribute-modal'; type AttributeControlProps = { value: ProductAttribute[]; + onAdd?: ( attribute: EnhancedProductAttribute[] ) => void; onChange: ( value: ProductAttribute[] ) => void; - // TODO: should we support an 'any' option to show all attributes? - attributeType?: 'regular' | 'for-variations'; + onEdit?: ( attribute: ProductAttribute ) => void; + onRemove?: ( attribute: ProductAttribute ) => void; + onRemoveCancel?: ( attribute: ProductAttribute ) => void; + onNewModalCancel?: () => void; + onNewModalClose?: () => void; + onNewModalOpen?: () => void; + onEditModalCancel?: ( attribute?: ProductAttribute ) => void; + onEditModalClose?: ( attribute?: ProductAttribute ) => void; + onEditModalOpen?: ( attribute?: ProductAttribute ) => void; + uiStrings?: { + emptyStateSubtitle?: string; + newAttributeListItemLabel?: string; + newAttributeModalTitle?: string; + globalAttributeHelperMessage: string; + }; }; export const AttributeControl: React.FC< AttributeControlProps > = ( { value, - attributeType = 'regular', + onAdd = () => {}, onChange, + onEdit = () => {}, + onNewModalCancel = () => {}, + onNewModalClose = () => {}, + onNewModalOpen = () => {}, + onEditModalCancel = () => {}, + onEditModalClose = () => {}, + onEditModalOpen = () => {}, + onRemove = () => {}, + onRemoveCancel = () => {}, + uiStrings = { + newAttributeModalTitle: undefined, + emptyStateSubtitle: undefined, + newAttributeListItemLabel: undefined, + globalAttributeHelperMessage: __( + `You can change the attribute's name in {{link}}Attributes{{/link}}.`, + 'woocommerce' + ), + }, } ) => { - const [ showAddAttributeModal, setShowAddAttributeModal ] = - useState( false ); - const [ editingAttributeId, setEditingAttributeId ] = useState< + const [ isNewModalVisible, setIsNewModalVisible ] = useState( false ); + const [ currentAttributeId, setCurrentAttributeId ] = useState< null | string >( null ); - const isOnlyForVariations = attributeType === 'for-variations'; - - const newAttributeProps = { variation: isOnlyForVariations }; - - const CANCEL_BUTTON_EVENT_NAME = isOnlyForVariations - ? 'product_add_options_modal_cancel_button_click' - : 'product_add_attributes_modal_cancel_button_click'; - - const fetchAttributeId = ( attribute: { id: number; name: string } ) => - `${ attribute.id }-${ attribute.name }`; - const handleChange = ( newAttributes: EnhancedProductAttribute[] ) => { onChange( newAttributes.map( ( attr ) => { @@ -74,79 +94,89 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( { ); }; - const onRemove = ( attribute: ProductAttribute ) => { + const handleRemove = ( attribute: ProductAttribute ) => { // eslint-disable-next-line no-alert if ( window.confirm( __( 'Remove this attribute?', 'woocommerce' ) ) ) { - recordEvent( - 'product_remove_attribute_confirmation_confirm_click' - ); handleChange( value.filter( ( attr ) => - fetchAttributeId( attr ) !== - fetchAttributeId( attribute ) + getAttributeId( attr ) !== getAttributeId( attribute ) ) ); - } else { - recordEvent( 'product_remove_attribute_confirmation_cancel_click' ); + onRemove( attribute ); + return; } + onRemoveCancel( attribute ); }; - const onAddNewAttributes = ( - newAttributes: EnhancedProductAttribute[] - ) => { + const openNewModal = () => { + setIsNewModalVisible( true ); + onNewModalOpen(); + }; + + const closeNewModal = () => { + setIsNewModalVisible( false ); + onNewModalClose(); + }; + + const openEditModal = ( attribute: ProductAttribute ) => { + setCurrentAttributeId( getAttributeId( attribute ) ); + onEditModalOpen( attribute ); + }; + + const closeEditModal = ( attribute: ProductAttribute ) => { + setCurrentAttributeId( null ); + onEditModalClose( attribute ); + }; + + const handleAdd = ( newAttributes: EnhancedProductAttribute[] ) => { handleChange( [ - ...( value || [] ), - ...newAttributes - .filter( - ( newAttr ) => - ! ( value || [] ).find( ( attr ) => - newAttr.id === 0 - ? newAttr.name === attr.name // check name if custom attribute = id === 0. - : attr.id === newAttr.id - ) - ) - .map( ( newAttr, index ) => { - return { - ...newAttributeProps, - ...newAttr, - position: ( value || [] ).length + index, - }; - } ), + ...value, + ...newAttributes.filter( + ( newAttr ) => + ! value.find( + ( attr ) => + getAttributeId( newAttr ) === getAttributeId( attr ) + ) + ), ] ); - recordEvent( 'product_add_attributes_modal_add_button_click' ); - setShowAddAttributeModal( false ); + onAdd( newAttributes ); + closeNewModal(); + }; + + const handleEdit = ( updatedAttribute: ProductAttribute ) => { + const updatedAttributes = value.map( ( attr ) => { + if ( + getAttributeId( attr ) === getAttributeId( updatedAttribute ) + ) { + return updatedAttribute; + } + + return attr; + } ); + + onEdit( updatedAttribute ); + handleChange( updatedAttributes ); + closeEditModal( updatedAttribute ); }; if ( ! value.length ) { return ( <> { - recordEvent( - 'product_add_first_attribute_button_click' - ); - setShowAddAttributeModal( true ); - } } - subtitle={ - isOnlyForVariations - ? __( 'No options yet', 'woocommerce' ) - : undefined - } + addNewLabel={ uiStrings.newAttributeModalTitle } + onNewClick={ () => openNewModal() } + subtitle={ uiStrings.emptyStateSubtitle } /> - { showAddAttributeModal && ( - { - recordEvent( CANCEL_BUTTON_EVENT_NAME ); - setShowAddAttributeModal( false ); + closeNewModal(); + onNewModalCancel(); } } - onAdd={ onAddNewAttributes } + onAdd={ handleAdd } selectedAttributeIds={ [] } + title={ uiStrings.newAttributeModalTitle } /> ) } @@ -167,20 +197,10 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( { {} as Record< number | string, ProductAttribute > ); - const editingAttribute = value.find( - ( attr ) => fetchAttributeId( attr ) === editingAttributeId + const currentAttribute = value.find( + ( attr ) => getAttributeId( attr ) === currentAttributeId ) as EnhancedProductAttribute; - const editAttributeCopy = isOnlyForVariations - ? __( - `You can change the option's name in {{link}}Attributes{{/link}}.`, - 'woocommerce' - ) - : __( - `You can change the attribute's name in {{link}}Attributes{{/link}}.`, - 'woocommerce' - ); - return (
= ( { { sortedAttributes.map( ( attr ) => ( - setEditingAttributeId( fetchAttributeId( attr ) ) - } - onRemoveClick={ () => onRemove( attr ) } + key={ getAttributeId( attr ) } + onEditClick={ () => openEditModal( attr ) } + onRemoveClick={ () => handleRemove( attr ) } /> ) ) } - { - recordEvent( - isOnlyForVariations - ? 'product_add_option_button' - : 'product_add_attribute_button' - ); - setShowAddAttributeModal( true ); - } } + openNewModal() } /> - { showAddAttributeModal && ( - { - recordEvent( CANCEL_BUTTON_EVENT_NAME ); - setShowAddAttributeModal( false ); + closeNewModal(); + onNewModalCancel(); } } - onAdd={ onAddNewAttributes } + onAdd={ handleAdd } selectedAttributeIds={ value.map( ( attr ) => attr.id ) } /> ) } - { editingAttribute && ( + { currentAttribute && ( = ( { ), }, } ) } - onCancel={ () => setEditingAttributeId( null ) } - onEdit={ ( changedAttribute ) => { - const newAttributesSet = [ ...value ]; - const changedAttributeIndex: number = - newAttributesSet.findIndex( ( attr ) => - attr.id !== 0 - ? attr.id === changedAttribute.id - : attr.name === changedAttribute.name - ); - - newAttributesSet.splice( - changedAttributeIndex, - 1, - changedAttribute - ); - - handleChange( newAttributesSet ); - setEditingAttributeId( null ); + onCancel={ () => { + closeEditModal( currentAttribute ); + onEditModalCancel( currentAttribute ); } } - attribute={ editingAttribute } + onEdit={ ( updatedAttribute ) => { + handleEdit( updatedAttribute ); + } } + attribute={ currentAttribute } /> ) }
diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-control/edit-attribute-modal.scss b/plugins/woocommerce-admin/client/products/fields/attribute-control/edit-attribute-modal.scss index 420debdc77a..8595c313e35 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-control/edit-attribute-modal.scss +++ b/plugins/woocommerce-admin/client/products/fields/attribute-control/edit-attribute-modal.scss @@ -1,5 +1,13 @@ .woocommerce-edit-attribute-modal { overflow: visible; + + &__buttons { + margin-top: $gap-larger; + display: flex; + flex-direction: row; + gap: $gap-smaller; + justify-content: flex-end; + } } .woocommerce-edit-attribute-modal__body { diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-control/edit-attribute-modal.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-control/edit-attribute-modal.tsx index 53da1cec567..836720e65e7 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-control/edit-attribute-modal.tsx +++ b/plugins/woocommerce-admin/client/products/fields/attribute-control/edit-attribute-modal.tsx @@ -136,7 +136,7 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
-
+