From 6a83e8d301d51d7ea114b146695df45d8f1a2a1f Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Wed, 18 Sep 2024 18:06:38 +0800 Subject: [PATCH 01/20] Fix deprecation notice in coming soon and store-only mode (#51474) * Wrap parse_str under a check * Changelog --- .../changelog/fix-51472-fix-deprecation-notice | 4 ++++ plugins/woocommerce/src/Admin/WCAdminHelper.php | 12 +++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-51472-fix-deprecation-notice diff --git a/plugins/woocommerce/changelog/fix-51472-fix-deprecation-notice b/plugins/woocommerce/changelog/fix-51472-fix-deprecation-notice new file mode 100644 index 00000000000..79ce818f52b --- /dev/null +++ b/plugins/woocommerce/changelog/fix-51472-fix-deprecation-notice @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Wrap parse_str under a check to resolve deprecation notice diff --git a/plugins/woocommerce/src/Admin/WCAdminHelper.php b/plugins/woocommerce/src/Admin/WCAdminHelper.php index 9e234762eb8..768102e35fb 100644 --- a/plugins/woocommerce/src/Admin/WCAdminHelper.php +++ b/plugins/woocommerce/src/Admin/WCAdminHelper.php @@ -154,11 +154,13 @@ class WCAdminHelper { 'post_type' => 'product', ); - parse_str( wp_parse_url( $url, PHP_URL_QUERY ), $url_params ); - - foreach ( $params as $key => $param ) { - if ( isset( $url_params[ $key ] ) && $url_params[ $key ] === $param ) { - return true; + $query_string = wp_parse_url( $url, PHP_URL_QUERY ); + if ( $query_string ) { + parse_str( $query_string, $url_params ); + foreach ( $params as $key => $param ) { + if ( isset( $url_params[ $key ] ) && $url_params[ $key ] === $param ) { + return true; + } } } From 3df48f2bd75550d1252cc3ba0610e6dac3183b24 Mon Sep 17 00:00:00 2001 From: Naman Malhotra Date: Wed, 18 Sep 2024 14:12:23 +0300 Subject: [PATCH 02/20] Revert low stock notification changes (#51441) * Revert - Change when stock notif emails are triggered * added changelog * restored comment * restored more comment * lint fixes * Add docblock. --------- Co-authored-by: Vedanshu Jain --- .../changelog/revert-low-stock-notification | 4 + .../class-wc-product-data-store-cpt.php | 24 ++--- .../includes/wc-stock-functions.php | 34 ++++-- .../php/includes/wc-stock-functions-tests.php | 100 ------------------ 4 files changed, 41 insertions(+), 121 deletions(-) create mode 100644 plugins/woocommerce/changelog/revert-low-stock-notification diff --git a/plugins/woocommerce/changelog/revert-low-stock-notification b/plugins/woocommerce/changelog/revert-low-stock-notification new file mode 100644 index 00000000000..3ec370d906f --- /dev/null +++ b/plugins/woocommerce/changelog/revert-low-stock-notification @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Revert - changes related to low stock product notification diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php index b5a611d6f8a..b6da3a8e879 100644 --- a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php +++ b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php @@ -656,21 +656,21 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da // Fire actions to let 3rd parties know the stock is about to be changed. if ( $product->is_type( 'variation' ) ) { /** - * Action to signal that the value of 'stock_quantity' for a variation is about to change. - * - * @param WC_Product $product The variation whose stock is about to change. - * - * @since 4.9 - */ + * Action to signal that the value of 'stock_quantity' for a variation is about to change. + * + * @since 4.9 + * + * @param int $product The variation whose stock is about to change. + */ do_action( 'woocommerce_variation_before_set_stock', $product ); } else { /** - * Action to signal that the value of 'stock_quantity' for a product is about to change. - * - * @param WC_Product $product The product whose stock is about to change. - * - * @since 4.9 - */ + * Action to signal that the value of 'stock_quantity' for a product is about to change. + * + * @since 4.9 + * + * @param int $product The product whose stock is about to change. + */ do_action( 'woocommerce_product_before_set_stock', $product ); } break; diff --git a/plugins/woocommerce/includes/wc-stock-functions.php b/plugins/woocommerce/includes/wc-stock-functions.php index 1489a0e6630..e81b31e8c50 100644 --- a/plugins/woocommerce/includes/wc-stock-functions.php +++ b/plugins/woocommerce/includes/wc-stock-functions.php @@ -242,10 +242,31 @@ function wc_trigger_stock_change_notifications( $order, $changes ) { return; } - $order_notes = array(); + $order_notes = array(); + $no_stock_amount = absint( get_option( 'woocommerce_notify_no_stock_amount', 0 ) ); foreach ( $changes as $change ) { - $order_notes[] = $change['product']->get_formatted_name() . ' ' . $change['from'] . '→' . $change['to']; + $order_notes[] = $change['product']->get_formatted_name() . ' ' . $change['from'] . '→' . $change['to']; + $low_stock_amount = absint( wc_get_low_stock_amount( wc_get_product( $change['product']->get_id() ) ) ); + if ( $change['to'] <= $no_stock_amount ) { + /** + * Action to signal that the value of 'stock_quantity' for a variation is about to change. + * + * @since 4.9 + * + * @param int $product The variation whose stock is about to change. + */ + do_action( 'woocommerce_no_stock', wc_get_product( $change['product']->get_id() ) ); + } elseif ( $change['to'] <= $low_stock_amount ) { + /** + * Action to signal that the value of 'stock_quantity' for a product is about to change. + * + * @since 4.9 + * + * @param int $product The product whose stock is about to change. + */ + do_action( 'woocommerce_low_stock', wc_get_product( $change['product']->get_id() ) ); + } if ( $change['to'] < 0 ) { /** @@ -312,8 +333,6 @@ function wc_trigger_stock_change_actions( $product ) { do_action( 'woocommerce_low_stock', $product ); } } -add_action( 'woocommerce_variation_set_stock', 'wc_trigger_stock_change_actions' ); -add_action( 'woocommerce_product_set_stock', 'wc_trigger_stock_change_actions' ); /** * Increase stock levels for items within an order. @@ -485,11 +504,8 @@ function wc_get_low_stock_amount( WC_Product $product ) { $low_stock_amount = $product->get_low_stock_amount(); if ( '' === $low_stock_amount && $product->is_type( 'variation' ) ) { - $parent_product = wc_get_product( $product->get_parent_id() ); - - if ( $parent_product instanceof WC_Product ) { - $low_stock_amount = $parent_product->get_low_stock_amount(); - } + $product = wc_get_product( $product->get_parent_id() ); + $low_stock_amount = $product->get_low_stock_amount(); } if ( '' === $low_stock_amount ) { diff --git a/plugins/woocommerce/tests/php/includes/wc-stock-functions-tests.php b/plugins/woocommerce/tests/php/includes/wc-stock-functions-tests.php index 90f312be98b..3e8bbb7bed8 100644 --- a/plugins/woocommerce/tests/php/includes/wc-stock-functions-tests.php +++ b/plugins/woocommerce/tests/php/includes/wc-stock-functions-tests.php @@ -356,104 +356,4 @@ class WC_Stock_Functions_Tests extends \WC_Unit_Test_Case { $this->assertIsIntAndEquals( $site_wide_low_stock_amount, wc_get_low_stock_amount( $var1 ) ); } - - /** - * @testdox Test that the `woocommerce_low_stock` action fires when a product stock hits the low stock threshold. - */ - public function test_wc_update_product_stock_low_stock_action() { - $product = WC_Helper_Product::create_simple_product(); - $product->set_manage_stock( true ); - $product->save(); - - $low_stock_amount = wc_get_low_stock_amount( $product ); - $initial_stock = $low_stock_amount + 2; - - wc_update_product_stock( $product->get_id(), $initial_stock ); - - $action_fired = false; - $callback = function () use ( &$action_fired ) { - $action_fired = true; - }; - add_action( 'woocommerce_low_stock', $callback ); - - // Test with `wc_update_product_stock`. - wc_update_product_stock( $product->get_id(), 1, 'decrease' ); - $this->assertFalse( $action_fired ); - wc_update_product_stock( $product->get_id(), 1, 'decrease' ); - $this->assertTrue( $action_fired ); - - $action_fired = false; - - // Test with the data store. - $product->set_stock_quantity( $initial_stock ); - $product->save(); - $this->assertFalse( $action_fired ); - $product->set_stock_quantity( $low_stock_amount ); - $product->save(); - $this->assertTrue( $action_fired ); - - remove_action( 'woocommerce_low_stock', $callback ); - } - - /** - * @testdox Test that the `woocommerce_no_stock` action fires when a product stock hits the no stock threshold. - */ - public function test_wc_update_product_stock_no_stock_action() { - $product = WC_Helper_Product::create_simple_product(); - $product->set_manage_stock( true ); - $product->save(); - - $no_stock_amount = get_option( 'woocommerce_notify_no_stock_amount', 0 ); - $initial_stock = $no_stock_amount + 2; - - wc_update_product_stock( $product->get_id(), $initial_stock ); - - $action_fired = false; - $callback = function () use ( &$action_fired ) { - $action_fired = true; - }; - add_action( 'woocommerce_no_stock', $callback ); - - // Test with `wc_update_product_stock`. - wc_update_product_stock( $product->get_id(), 1, 'decrease' ); - $this->assertFalse( $action_fired ); - wc_update_product_stock( $product->get_id(), 1, 'decrease' ); - $this->assertTrue( $action_fired ); - - $action_fired = false; - - // Test with the data store. - $product->set_stock_quantity( $initial_stock ); - $product->save(); - $this->assertFalse( $action_fired ); - $product->set_stock_quantity( $no_stock_amount ); - $product->save(); - $this->assertTrue( $action_fired ); - - remove_action( 'woocommerce_no_stock', $callback ); - } - - /** - * @testdox The wc_trigger_stock_change_actions function should only trigger actions if the product is set - * to manage stock. - */ - public function test_wc_trigger_stock_change_actions_bails_early_for_unmanaged_stock() { - $action_fired = false; - $callback = function () use ( &$action_fired ) { - $action_fired = true; - }; - add_action( 'woocommerce_no_stock', $callback ); - - $product = WC_Helper_Product::create_simple_product(); - - $this->assertFalse( $action_fired ); - - $product->set_manage_stock( true ); - $product->set_stock_quantity( 0 ); - $product->save(); - - $this->assertTrue( $action_fired ); - - remove_action( 'woocommerce_no_stock', $callback ); - } } From 16e072879bb8fcd062c53c47cdef8dd7b2d8a8cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9stor=20Soriano?= Date: Wed, 18 Sep 2024 13:15:15 +0200 Subject: [PATCH 03/20] Fix: "Import file path is invalid" in Windows (#51456) * Fix WC_Product_CSV_Importer_Controller::check_file_path failing on Windows --- plugins/woocommerce/changelog/pr-51456 | 4 ++++ .../importers/class-wc-product-csv-importer-controller.php | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/pr-51456 diff --git a/plugins/woocommerce/changelog/pr-51456 b/plugins/woocommerce/changelog/pr-51456 new file mode 100644 index 00000000000..9d71edd5fbf --- /dev/null +++ b/plugins/woocommerce/changelog/pr-51456 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix invalid path error in product importer in Windows diff --git a/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php index cadf53a7bc5..0ce9d749a2b 100644 --- a/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php +++ b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php @@ -125,6 +125,7 @@ class WC_Product_CSV_Importer_Controller { // Check that file is within an allowed location. if ( $is_valid_file ) { + $normalized_path = wp_normalize_path( $path ); $in_valid_location = false; $valid_locations = array(); $valid_locations[] = ABSPATH; @@ -135,7 +136,8 @@ class WC_Product_CSV_Importer_Controller { } foreach ( $valid_locations as $valid_location ) { - if ( 0 === stripos( $path, trailingslashit( realpath( $valid_location ) ) ) ) { + $normalized_location = wp_normalize_path( realpath( $valid_location ) ); + if ( 0 === stripos( $normalized_path, trailingslashit( $normalized_location ) ) ) { $in_valid_location = true; break; } From 4bc4649008d0ea0dc2c5409304a88b2b84f5308d Mon Sep 17 00:00:00 2001 From: Ivan Stojadinov Date: Wed, 18 Sep 2024 13:24:38 +0200 Subject: [PATCH 04/20] [e2e] External - Expand WPCOM suite, part 3 (#51422) * Skip "Restricted coupon management" - error 500 * Make `wooPatterns` element more specific * Skip "Merchant can view a list of all customers" - more customers due to using existing website * No LYS on WPCOM * Dismiss notice if shown * Expand WPCOM suite * Click on `Canada` * Click on `Canada` * Be more specific about `Delete` button * Add changefile(s) from automation for the following project(s): woocommerce * Skip two more tests - WC patterns and block * Remove usage of `networkidle` * Handle notice if present with `addLocatorHandler` * Fix ESLint issue --------- Co-authored-by: github-actions --- ...1422-e2e-external-expand-wpcom-suite-part3 | 4 + .../envs/default-wpcom/playwright.config.js | 10 + .../create-restricted-coupons.spec.js | 787 +++++++++--------- .../merchant/create-shipping-zones.spec.js | 37 +- .../create-woocommerce-blocks.spec.js | 9 +- .../create-woocommerce-patterns.spec.js | 13 +- .../tests/merchant/customer-list.spec.js | 2 +- .../merchant/customer-payment-page.spec.js | 11 + .../tests/merchant/launch-your-store.spec.js | 2 +- 9 files changed, 461 insertions(+), 414 deletions(-) create mode 100644 plugins/woocommerce/changelog/51422-e2e-external-expand-wpcom-suite-part3 diff --git a/plugins/woocommerce/changelog/51422-e2e-external-expand-wpcom-suite-part3 b/plugins/woocommerce/changelog/51422-e2e-external-expand-wpcom-suite-part3 new file mode 100644 index 00000000000..4eeb743cc9d --- /dev/null +++ b/plugins/woocommerce/changelog/51422-e2e-external-expand-wpcom-suite-part3 @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Expand the e2e suite we're running on WPCOM part #3. \ No newline at end of file diff --git a/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js b/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js index 27ec548c34a..a3e2135c0ad 100644 --- a/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js +++ b/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js @@ -23,6 +23,16 @@ config = { '**/merchant/create-order.spec.js', '**/merchant/create-page.spec.js', '**/merchant/create-post.spec.js', + '**/merchant/create-restricted-coupons.spec.js', + '**/merchant/create-shipping-classes.spec.js', + '**/merchant/create-shipping-zones.spec.js', + '**/merchant/create-woocommerce-blocks.spec.js', + '**/merchant/create-woocommerce-patterns.spec.js', + '**/merchant/customer-list.spec.js', + '**/merchant/customer-payment-page.spec.js', + '**/merchant/launch-your-store.spec.js', + '**/merchant/lost-password.spec.js', + '**/merchant/order-bulk-edit.spec.js', ], grepInvert: /@skip-on-default-wpcom/, }, diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-restricted-coupons.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-restricted-coupons.spec.js index 3fd0b4c1717..2d6f69e7eb0 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-restricted-coupons.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-restricted-coupons.spec.js @@ -103,397 +103,414 @@ const test = baseTest.extend( { }, } ); -test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => { - for ( const couponType of Object.keys( couponData ) ) { - test( `can create new ${ couponType } coupon`, async ( { - page, - coupon, - product, - } ) => { - // create basics for the coupon - await test.step( 'add new coupon', async () => { - await page.goto( - 'wp-admin/post-new.php?post_type=shop_coupon' - ); - await page - .getByLabel( 'Coupon code' ) - .fill( couponData[ couponType ].code ); - await page - .getByPlaceholder( 'Description (optional)' ) - .fill( couponData[ couponType ].description ); - await page - .getByPlaceholder( '0' ) - .fill( couponData[ couponType ].amount ); - await expect( page.getByText( 'Move to Trash' ) ).toBeVisible(); - } ); - - // set up the restrictions for each coupon type - // set minimum spend - if ( couponType === 'minimumSpend' ) { - await test.step( 'set minimum spend coupon', async () => { - await page - .getByRole( 'link', { - name: 'Usage restriction', - } ) - .click(); - await page - .getByPlaceholder( 'No minimum' ) - .fill( couponData[ couponType ].minSpend ); - } ); - } - // set maximum spend - if ( couponType === 'maximumSpend' ) { - await test.step( 'set maximum spend coupon', async () => { - await page - .getByRole( 'link', { - name: 'Usage restriction', - } ) - .click(); - await page - .getByPlaceholder( 'No maximum' ) - .fill( couponData[ couponType ].maxSpend ); - } ); - } - // set individual use - if ( couponType === 'individualUse' ) { - await test.step( 'set individual use coupon', async () => { - await page - .getByRole( 'link', { - name: 'Usage restriction', - } ) - .click(); - await page.getByLabel( 'Individual use only' ).check(); - } ); - } - // set exclude sale items - if ( couponType === 'excludeSaleItems' ) { - await test.step( 'set exclude sale items coupon', async () => { - await page - .getByRole( 'link', { - name: 'Usage restriction', - } ) - .click(); - await page.getByLabel( 'Exclude sale items' ).check(); - } ); - } - // set product categories - if ( couponType === 'productCategories' ) { - await test.step( 'set product categories coupon', async () => { - await page - .getByRole( 'link', { - name: 'Usage restriction', - } ) - .click(); - await page - .getByPlaceholder( 'Any category' ) - .pressSequentially( 'Uncategorized' ); - await page - .getByRole( 'option', { name: 'Uncategorized' } ) - .click(); - } ); - } - // set exclude product categories - if ( couponType === 'excludeProductCategories' ) { - await test.step( 'set exclude product categories coupon', async () => { - await page - .getByRole( 'link', { - name: 'Usage restriction', - } ) - .click(); - await page - .getByPlaceholder( 'No categories' ) - .pressSequentially( 'Uncategorized' ); - await page - .getByRole( 'option', { name: 'Uncategorized' } ) - .click(); - } ); - } - - // Skip Brands tests while behind a feature flag. - const skipBrandsTests = true; - - // set exclude product brands - if ( couponType === 'excludeProductBrands' && ! skipBrandsTests ) { - await test.step( 'set exclude product brands coupon', async () => { - await page - .getByRole( 'link', { - name: 'Usage restriction', - } ) - .click(); - await page - .getByPlaceholder( 'No brands' ) - .pressSequentially( 'WooCommerce Apparels' ); - await page - .getByRole( 'option', { name: 'WooCommerce Apparels' } ) - .click(); - } ); - } - // set products - if ( couponType === 'products' ) { - await test.step( 'set products coupon', async () => { - await page - .getByRole( 'link', { - name: 'Usage restriction', - } ) - .click(); - await page - .getByPlaceholder( 'Search for a product…' ) - .first() - .pressSequentially( product.name ); - await page - .getByRole( 'option', { name: product.name } ) - .click(); - } ); - } - // set exclude products - if ( couponType === 'excludeProducts' ) { - await test.step( 'set exclude products coupon', async () => { - await page - .getByRole( 'link', { - name: 'Usage restriction', - } ) - .click(); - await page - .getByPlaceholder( 'Search for a product…' ) - .last() - .pressSequentially( product.name ); - await page - .getByRole( 'option', { name: product.name } ) - .click(); - } ); - } - // set allowed emails - if ( couponType === 'allowedEmails' ) { - await test.step( 'set allowed emails coupon', async () => { - await page - .getByRole( 'link', { - name: 'Usage restriction', - } ) - .click(); - await page - .getByPlaceholder( 'No restrictions' ) - .fill( couponData[ couponType ].allowedEmails[ 0 ] ); - } ); - } - // set usage limit - if ( couponType === 'usageLimitPerCoupon' ) { - await test.step( 'set usage limit coupon', async () => { - await page - .getByRole( 'link', { name: 'Usage limits' } ) - .click(); - await page - .getByLabel( 'Usage limit per coupon' ) - .fill( couponData[ couponType ].usageLimit ); - } ); - } - // set usage limit per user - if ( couponType === 'usageLimitPerUser' ) { - await test.step( 'set usage limit per user coupon', async () => { - await page - .getByRole( 'link', { name: 'Usage limits' } ) - .click(); - await page - .getByLabel( 'Usage limit per user' ) - .fill( couponData[ couponType ].usageLimitPerUser ); - } ); - } - - // publish the coupon and retrieve the id - await test.step( 'publish the coupon', async () => { - await page - .getByRole( 'button', { name: 'Publish', exact: true } ) - .click(); - await expect( - page.getByText( 'Coupon updated.' ) - ).toBeVisible(); - coupon.id = page.url().match( /(?<=post=)\d+/ )[ 0 ]; - expect( coupon.id ).toBeDefined(); - } ); - - // verify the creation of the coupon and basic details - await test.step( 'verify coupon creation', async () => { - await page.goto( 'wp-admin/edit.php?post_type=shop_coupon' ); - await expect( - page.getByRole( 'cell', { - name: couponData[ couponType ].code, - } ) - ).toBeVisible(); - await expect( - page.getByRole( 'cell', { - name: couponData[ couponType ].description, - } ) - ).toBeVisible(); - await expect( - page.getByRole( 'cell', { - name: couponData[ couponType ].amount, - exact: true, - } ) - ).toBeVisible(); - - await page - .getByRole( 'link', { - name: couponData[ couponType ].code, - } ) - .first() - .click(); - } ); - - // verify the restrictions for each coupon type - // verify minimum spend - if ( couponType === 'minimumSpend' ) { - await test.step( 'verify minimum spend coupon', async () => { - await page - .getByRole( 'link', { - name: 'Usage restriction', - } ) - .click(); - await expect( - page.getByPlaceholder( 'No minimum' ) - ).toHaveValue( couponData[ couponType ].minSpend ); - } ); - } - - // verify maximum spend - if ( couponType === 'maximumSpend' ) { - await test.step( 'verify maximum spend coupon', async () => { - await page - .getByRole( 'link', { - name: 'Usage restriction', - } ) - .click(); - await expect( - page.getByPlaceholder( 'No maximum' ) - ).toHaveValue( couponData[ couponType ].maxSpend ); - } ); - } - - // verify individual use - if ( couponType === 'individualUse' ) { - await test.step( 'verify individual use coupon', async () => { - await page - .getByRole( 'link', { - name: 'Usage restriction', - } ) - .click(); - await expect( - page.getByLabel( 'Individual use only' ) - ).toBeChecked(); - } ); - } - - // verify exclude sale items - if ( couponType === 'excludeSaleItems' ) { - await test.step( 'verify exclude sale items coupon', async () => { - await page - .getByRole( 'link', { - name: 'Usage restriction', - } ) - .click(); - await expect( - page.getByLabel( 'Exclude sale items' ) - ).toBeChecked(); - } ); - } - - // verify product categories - if ( couponType === 'productCategories' ) { - await test.step( 'verify product categories coupon', async () => { - await page - .getByRole( 'link', { - name: 'Usage restriction', - } ) - .click(); - await expect( - page.getByRole( 'listitem', { - name: 'Uncategorized', - } ) - ).toBeVisible(); - } ); - } - - // verify exclude product categories - if ( couponType === 'excludeProductCategories' ) { - await test.step( 'verify exclude product categories coupon', async () => { - await page - .getByRole( 'link', { - name: 'Usage restriction', - } ) - .click(); - await expect( - page.getByRole( 'listitem', { - name: 'Uncategorized', - } ) - ).toBeVisible(); - } ); - } - - // verify products - if ( couponType === 'products' ) { - await test.step( 'verify products coupon', async () => { - await page - .getByRole( 'link', { - name: 'Usage restriction', - } ) - .click(); - await expect( - page.getByRole( 'listitem', { name: product.name } ) - ).toBeVisible(); - } ); - } - - // verify exclude products - if ( couponType === 'excludeProducts' ) { - await test.step( 'verify exclude products coupon', async () => { - await page - .getByRole( 'link', { - name: 'Usage restriction', - } ) - .click(); - await expect( - page.getByRole( 'listitem', { name: product.name } ) - ).toBeVisible(); - } ); - } - - // verify allowed emails - if ( couponType === 'allowedEmails' ) { - await test.step( 'verify allowed emails coupon', async () => { - await page - .getByRole( 'link', { - name: 'Usage restriction', - } ) - .click(); - await expect( - page.getByPlaceholder( 'No restrictions' ) - ).toHaveValue( - couponData[ couponType ].allowedEmails[ 0 ] +test.describe( + 'Restricted coupon management', + { tag: [ '@services', '@skip-on-default-wpcom' ] }, + () => { + for ( const couponType of Object.keys( couponData ) ) { + test( `can create new ${ couponType } coupon`, async ( { + page, + coupon, + product, + } ) => { + // create basics for the coupon + await test.step( 'add new coupon', async () => { + await page.goto( + 'wp-admin/post-new.php?post_type=shop_coupon' ); - } ); - } - - // verify usage limit - if ( couponType === 'usageLimitPerCoupon' ) { - await test.step( 'verify usage limit coupon', async () => { await page - .getByRole( 'link', { name: 'Usage limits' } ) + .getByLabel( 'Coupon code' ) + .fill( couponData[ couponType ].code ); + await page + .getByPlaceholder( 'Description (optional)' ) + .fill( couponData[ couponType ].description ); + await page + .getByPlaceholder( '0' ) + .fill( couponData[ couponType ].amount ); + await expect( + page.getByText( 'Move to Trash' ) + ).toBeVisible(); + } ); + + // set up the restrictions for each coupon type + // set minimum spend + if ( couponType === 'minimumSpend' ) { + await test.step( 'set minimum spend coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await page + .getByPlaceholder( 'No minimum' ) + .fill( couponData[ couponType ].minSpend ); + } ); + } + // set maximum spend + if ( couponType === 'maximumSpend' ) { + await test.step( 'set maximum spend coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await page + .getByPlaceholder( 'No maximum' ) + .fill( couponData[ couponType ].maxSpend ); + } ); + } + // set individual use + if ( couponType === 'individualUse' ) { + await test.step( 'set individual use coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await page.getByLabel( 'Individual use only' ).check(); + } ); + } + // set exclude sale items + if ( couponType === 'excludeSaleItems' ) { + await test.step( 'set exclude sale items coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await page.getByLabel( 'Exclude sale items' ).check(); + } ); + } + // set product categories + if ( couponType === 'productCategories' ) { + await test.step( 'set product categories coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await page + .getByPlaceholder( 'Any category' ) + .pressSequentially( 'Uncategorized' ); + await page + .getByRole( 'option', { name: 'Uncategorized' } ) + .click(); + } ); + } + // set exclude product categories + if ( couponType === 'excludeProductCategories' ) { + await test.step( 'set exclude product categories coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await page + .getByPlaceholder( 'No categories' ) + .pressSequentially( 'Uncategorized' ); + await page + .getByRole( 'option', { name: 'Uncategorized' } ) + .click(); + } ); + } + + // Skip Brands tests while behind a feature flag. + const skipBrandsTests = true; + + // set exclude product brands + if ( + couponType === 'excludeProductBrands' && + ! skipBrandsTests + ) { + await test.step( 'set exclude product brands coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await page + .getByPlaceholder( 'No brands' ) + .pressSequentially( 'WooCommerce Apparels' ); + await page + .getByRole( 'option', { + name: 'WooCommerce Apparels', + } ) + .click(); + } ); + } + // set products + if ( couponType === 'products' ) { + await test.step( 'set products coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await page + .getByPlaceholder( 'Search for a product…' ) + .first() + .pressSequentially( product.name ); + await page + .getByRole( 'option', { name: product.name } ) + .click(); + } ); + } + // set exclude products + if ( couponType === 'excludeProducts' ) { + await test.step( 'set exclude products coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await page + .getByPlaceholder( 'Search for a product…' ) + .last() + .pressSequentially( product.name ); + await page + .getByRole( 'option', { name: product.name } ) + .click(); + } ); + } + // set allowed emails + if ( couponType === 'allowedEmails' ) { + await test.step( 'set allowed emails coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await page + .getByPlaceholder( 'No restrictions' ) + .fill( + couponData[ couponType ].allowedEmails[ 0 ] + ); + } ); + } + // set usage limit + if ( couponType === 'usageLimitPerCoupon' ) { + await test.step( 'set usage limit coupon', async () => { + await page + .getByRole( 'link', { name: 'Usage limits' } ) + .click(); + await page + .getByLabel( 'Usage limit per coupon' ) + .fill( couponData[ couponType ].usageLimit ); + } ); + } + // set usage limit per user + if ( couponType === 'usageLimitPerUser' ) { + await test.step( 'set usage limit per user coupon', async () => { + await page + .getByRole( 'link', { name: 'Usage limits' } ) + .click(); + await page + .getByLabel( 'Usage limit per user' ) + .fill( couponData[ couponType ].usageLimitPerUser ); + } ); + } + + // publish the coupon and retrieve the id + await test.step( 'publish the coupon', async () => { + await page + .getByRole( 'button', { name: 'Publish', exact: true } ) .click(); await expect( - page.getByLabel( 'Usage limit per coupon' ) - ).toHaveValue( couponData[ couponType ].usageLimit ); + page.getByText( 'Coupon updated.' ) + ).toBeVisible(); + coupon.id = page.url().match( /(?<=post=)\d+/ )[ 0 ]; + expect( coupon.id ).toBeDefined(); } ); - } - // verify usage limit per user - if ( couponType === 'usageLimitPerUser' ) { - await test.step( 'verify usage limit per user coupon', async () => { - await page - .getByRole( 'link', { name: 'Usage limits' } ) - .click(); + // verify the creation of the coupon and basic details + await test.step( 'verify coupon creation', async () => { + await page.goto( + 'wp-admin/edit.php?post_type=shop_coupon' + ); await expect( - page.getByLabel( 'Usage limit per user' ) - ).toHaveValue( couponData[ couponType ].usageLimitPerUser ); + page.getByRole( 'cell', { + name: couponData[ couponType ].code, + } ) + ).toBeVisible(); + await expect( + page.getByRole( 'cell', { + name: couponData[ couponType ].description, + } ) + ).toBeVisible(); + await expect( + page.getByRole( 'cell', { + name: couponData[ couponType ].amount, + exact: true, + } ) + ).toBeVisible(); + + await page + .getByRole( 'link', { + name: couponData[ couponType ].code, + } ) + .first() + .click(); } ); - } - } ); + + // verify the restrictions for each coupon type + // verify minimum spend + if ( couponType === 'minimumSpend' ) { + await test.step( 'verify minimum spend coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await expect( + page.getByPlaceholder( 'No minimum' ) + ).toHaveValue( couponData[ couponType ].minSpend ); + } ); + } + + // verify maximum spend + if ( couponType === 'maximumSpend' ) { + await test.step( 'verify maximum spend coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await expect( + page.getByPlaceholder( 'No maximum' ) + ).toHaveValue( couponData[ couponType ].maxSpend ); + } ); + } + + // verify individual use + if ( couponType === 'individualUse' ) { + await test.step( 'verify individual use coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await expect( + page.getByLabel( 'Individual use only' ) + ).toBeChecked(); + } ); + } + + // verify exclude sale items + if ( couponType === 'excludeSaleItems' ) { + await test.step( 'verify exclude sale items coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await expect( + page.getByLabel( 'Exclude sale items' ) + ).toBeChecked(); + } ); + } + + // verify product categories + if ( couponType === 'productCategories' ) { + await test.step( 'verify product categories coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await expect( + page.getByRole( 'listitem', { + name: 'Uncategorized', + } ) + ).toBeVisible(); + } ); + } + + // verify exclude product categories + if ( couponType === 'excludeProductCategories' ) { + await test.step( 'verify exclude product categories coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await expect( + page.getByRole( 'listitem', { + name: 'Uncategorized', + } ) + ).toBeVisible(); + } ); + } + + // verify products + if ( couponType === 'products' ) { + await test.step( 'verify products coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await expect( + page.getByRole( 'listitem', { name: product.name } ) + ).toBeVisible(); + } ); + } + + // verify exclude products + if ( couponType === 'excludeProducts' ) { + await test.step( 'verify exclude products coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await expect( + page.getByRole( 'listitem', { name: product.name } ) + ).toBeVisible(); + } ); + } + + // verify allowed emails + if ( couponType === 'allowedEmails' ) { + await test.step( 'verify allowed emails coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await expect( + page.getByPlaceholder( 'No restrictions' ) + ).toHaveValue( + couponData[ couponType ].allowedEmails[ 0 ] + ); + } ); + } + + // verify usage limit + if ( couponType === 'usageLimitPerCoupon' ) { + await test.step( 'verify usage limit coupon', async () => { + await page + .getByRole( 'link', { name: 'Usage limits' } ) + .click(); + await expect( + page.getByLabel( 'Usage limit per coupon' ) + ).toHaveValue( couponData[ couponType ].usageLimit ); + } ); + } + + // verify usage limit per user + if ( couponType === 'usageLimitPerUser' ) { + await test.step( 'verify usage limit per user coupon', async () => { + await page + .getByRole( 'link', { name: 'Usage limits' } ) + .click(); + await expect( + page.getByLabel( 'Usage limit per user' ) + ).toHaveValue( + couponData[ couponType ].usageLimitPerUser + ); + } ); + } + } ); + } } -} ); +); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-shipping-zones.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-shipping-zones.spec.js index 93bc2a1d605..37dbf85ec6d 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-shipping-zones.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-shipping-zones.spec.js @@ -60,8 +60,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => { // this shipping zone already exists, don't create it } else { await page.goto( - 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new', - { waitUntil: 'networkidle' } + 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new' ); await page .getByPlaceholder( 'Zone name' ) @@ -92,10 +91,8 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => { .getByRole( 'button', { name: 'Continue' } ) .last() .click(); - await page.waitForLoadState( 'networkidle' ); await page.locator( '#btn-ok' ).click(); - await page.waitForLoadState( 'networkidle' ); await expect( page @@ -132,8 +129,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => { // this shipping zone already exists, don't create it } else { await page.goto( - 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new', - { waitUntil: 'networkidle' } + 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new' ); await page .getByPlaceholder( 'Zone name' ) @@ -159,10 +155,8 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => { .getByRole( 'button', { name: 'Continue' } ) .last() .click(); - await page.waitForLoadState( 'networkidle' ); await page.locator( '#btn-ok' ).click(); - await page.waitForLoadState( 'networkidle' ); await expect( page @@ -196,8 +190,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => { // this shipping zone already exists, don't create it } else { await page.goto( - 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new', - { waitUntil: 'networkidle' } + 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new' ); await page .getByPlaceholder( 'Zone name' ) @@ -209,7 +202,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => { input.click(); input.fill( 'Canada' ); - await page.getByText( 'Canada' ).last().click(); + await page.getByLabel( 'Canada', { exact: true } ).click(); // Close dropdown await page.getByPlaceholder( 'Zone name' ).click(); @@ -222,10 +215,8 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => { .getByRole( 'button', { name: 'Continue' } ) .last() .click(); - await page.waitForLoadState( 'networkidle' ); await page.locator( '#btn-ok' ).click(); - await page.waitForLoadState( 'networkidle' ); await expect( page @@ -240,7 +231,6 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => { .click(); await page.getByLabel( 'Cost', { exact: true } ).fill( '10' ); await page.getByRole( 'button', { name: 'Save' } ).last().click(); - await page.waitForLoadState( 'networkidle' ); await page.goto( 'wp-admin/admin.php?page=wc-settings&tab=shipping' @@ -342,8 +332,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => { // this shipping zone already exists, don't create it } else { await page.goto( - 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new', - { waitUntil: 'networkidle' } + 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new' ); await page.locator( '#zone_name' ).fill( shippingZoneNameFlatRate ); @@ -353,7 +342,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => { input.click(); input.type( 'Canada' ); - await page.getByText( 'Canada' ).last().click(); + await page.getByLabel( 'Canada', { exact: true } ).click(); // Close dropdown await page.keyboard.press( 'Escape' ); @@ -366,10 +355,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => { .last() .click(); - await page.waitForLoadState( 'networkidle' ); - await page.locator( '#btn-ok' ).click(); - await page.waitForLoadState( 'networkidle' ); await expect( page @@ -384,13 +370,17 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => { .click(); await page.locator( '#woocommerce_flat_rate_cost' ).fill( '10' ); await page.locator( '#btn-ok' ).click(); - await page.waitForLoadState( 'networkidle' ); - await page.locator( 'text=Delete' ).waitFor(); + await expect( + page.getByRole( 'cell', { name: 'Edit | Delete', exact: true } ) + ).toBeVisible(); page.on( 'dialog', ( dialog ) => dialog.accept() ); - await page.locator( 'text=Delete' ).click(); + await page + .getByRole( 'cell', { name: 'Edit | Delete', exact: true } ) + .locator( 'text=Delete' ) + .click(); await expect( page.locator( '.wc-shipping-zone-method-blank-state' ) @@ -482,7 +472,6 @@ test.describe( 'Verifies shipping options from customer perspective', () => { await context.clearCookies(); await page.goto( `/shop/?add-to-cart=${ productId }` ); - await page.waitForLoadState( 'networkidle' ); } ); test.afterAll( async ( { baseURL } ) => { diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-blocks.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-blocks.spec.js index 7ade737367f..05b21525efa 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-blocks.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-blocks.spec.js @@ -52,7 +52,14 @@ const test = baseTest.extend( { test.describe( 'Add WooCommerce Blocks Into Page', - { tag: [ '@gutenberg', '@services', '@skip-on-default-pressable' ] }, + { + tag: [ + '@gutenberg', + '@services', + '@skip-on-default-pressable', + '@skip-on-default-wpcom', + ], + }, () => { test.beforeAll( async ( { api } ) => { // add product attribute diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-patterns.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-patterns.spec.js index ee6d56b0d9c..671788097b4 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-patterns.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-patterns.spec.js @@ -28,7 +28,14 @@ const test = baseTest.extend( { test.describe( 'Add WooCommerce Patterns Into Page', - { tag: [ '@gutenberg', '@services', '@skip-on-default-pressable' ] }, + { + tag: [ + '@gutenberg', + '@services', + '@skip-on-default-pressable', + '@skip-on-default-wpcom', + ], + }, () => { test( 'can insert WooCommerce patterns into page', async ( { page, @@ -86,7 +93,9 @@ test.describe( // check some elements from added patterns for ( let i = 1; i < wooPatterns.length; i++ ) { await expect( - page.getByText( `${ wooPatterns[ i ].button }` ) + page.getByRole( 'link', { + name: `${ wooPatterns[ i ].button }`, + } ) ).toBeVisible(); } } ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-list.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-list.spec.js index 1b8cb58846a..3a4501f9871 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-list.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-list.spec.js @@ -85,7 +85,7 @@ test.describe( 'Merchant > Customer List', { tag: '@services' }, () => { test( 'Merchant can view a list of all customers, filter and download', - { tag: '@skip-on-default-pressable' }, + { tag: [ '@skip-on-default-pressable', '@skip-on-default-wpcom' ] }, async ( { page, customers } ) => { await test.step( 'Go to the customers reports page', async () => { const responsePromise = page.waitForResponse( diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-payment-page.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-payment-page.spec.js index 652d819edd2..adc673add2b 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-payment-page.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-payment-page.spec.js @@ -110,6 +110,17 @@ test.describe( await test.step( 'Select payment method and pay for the order', async () => { // explicitly select the payment method await page.getByText( 'Direct bank transfer' ).click(); + + // Handle notice if present + await page.addLocatorHandler( + page.getByRole( 'link', { name: 'Dismiss' } ), + async () => { + await page + .getByRole( 'link', { name: 'Dismiss' } ) + .click(); + } + ); + // pay for the order await page .getByRole( 'button', { name: 'Pay for order' } ) diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/launch-your-store.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/launch-your-store.spec.js index cfef43102a9..4d40109a591 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/launch-your-store.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/launch-your-store.spec.js @@ -4,7 +4,7 @@ const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; test.describe( 'Launch Your Store - logged in', - { tag: [ '@gutenberg', '@services' ] }, + { tag: [ '@gutenberg', '@services', '@skip-on-default-wpcom' ] }, () => { test.use( { storageState: process.env.ADMINSTATE } ); From ce66b55bc5d5e982a54b8fa1cd94a5b52194bd5b Mon Sep 17 00:00:00 2001 From: Boro Sitnikovski Date: Wed, 18 Sep 2024 14:14:30 +0200 Subject: [PATCH 05/20] In app search improvements feature branch (#51413) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add search results count to the in-app marketplace (#51266) * Add searchResults to context * Use setSearchResults in Content * Add ribbons to the tabs * Changelog * Use setState as the function name * Only show ribbon counts when there's an active search * Refactor how 'setSearchResultsCount' is used (h/t @mcliwanow) * Don't populate initial search results * Unify css styling * Marketplace: bring back the loading state (#51342) * Marketplace: bring back the loading state * Add changefile(s) from automation for the following project(s): woocommerce --------- Co-authored-by: github-actions * Remove in-app marketplace Search results tab and unify results into existing tabs (#51297) * Remove search results component and any references to it * Persist current tab for searching, or default to extensions if tab is not set * Persist term when switching across tabs * Lint * When a search is initiated, fetch all categories to keep the tab counts up to date. The necessary filtering to display data to the current screen will be performed on the frontend. * Apply correct colors to the tabs, as per design * Beyond query.term, also rely on isLoading so that search result counts don't jump * Address an issue when the user searches for something that returns no results in the business services tab * Changelog * Addressed :) * Change key to category * Fix category filter being broken Whenever a category is requested, we need to do an additional request with the category param being the current category (overriding extensions/theme/business services). Ideally the backend API would make a distinction between type (theme/extension/business service) and category, but this hack should do for now. * Lint * Remove unused variables h/t @KokkieH * Lint * Revert "Lint" This reverts commit 0b2d2dca6de5087b3b0ae5599f8bb0114cebc4ab. * Actually fix lint without introducing infinite loop Reproducible at http://localhost:8080/wp-admin/admin.php?page=wc-admin&term=payments&tab=extensions&path=%2Fextensions&category=customer-service * Show category selector even if there are no results displayed * Update comment to be less misleading * Query isn't used here * Update Marketplace search component (#51313) * Update Search placeholder text * Replace search component with one from @wordpress/components * Make mobile search field font consistent with desktop * Add changefile(s) from automation for the following project(s): woocommerce * Handle import errors for SearchControl component --------- Co-authored-by: github-actions * Marketplace: update category selector (#51309) * Marketplace: update category selector Remove the dropdown on the desktop view and show all items, even if overflowing. Added helper buttons to scroll to the right to show more. * Add changefile(s) from automation for the following project(s): woocommerce * Marketplace: remove category sroll helpers from tabindex GitHub: https://github.com/woocommerce/woocommerce/pull/51309/files#r1758448638 * Marketplace: Remove selectedTab reference from product.tsx This is probably included due to the merge conflict * Marketplace: tweak category scroll button narrower --------- Co-authored-by: github-actions * Lint * Fix 2 lint errors * Fix another lint error (useMemo) h/t @KokkieH * Add load more button in-app (#51434) * Add additional fields returned by search API to marketplace types Ensure components have access to additional fields * Add LoadMoreButton component * Only render Load More button if there are additional pages of results * Fetch and display next page of results in Load More button is clicked * Simplify renderContent function to have less repetition - Hide load more button while fetching results * Improve loading of new products - Ensure keyboard focus goes to first new product after Load More is clicked * Add changefile(s) from automation for the following project(s): woocommerce * Add blank line to separate sections * Set category param based on current tab when loading more products * Improve busy-state screen reader text Co-authored-by: Boro Sitnikovski * Add missing dependency * Move getProductType() function to functions.tsx - Do not show load more button if isLoading state is true --------- Co-authored-by: github-actions Co-authored-by: Boro Sitnikovski * Rework the values used with `setSearchResultsCount` After https://github.com/Automattic/woocommerce.com/pull/21678/files we get a `totalProducts` so we can re-use that. Also remove setting the counts when paginating since we set them to the total. * Add search complete announcement h/t @KokkieH * Show update count only if greater than 0 h/t @andfinally * Switch to Extensions tab if on My subscriptions when searching * yoda --------- Co-authored-by: Cem Ünalan Co-authored-by: github-actions Co-authored-by: Herman --- .../category-selector/category-selector.scss | 26 +- .../category-selector/category-selector.tsx | 232 ++++++--- .../marketplace/components/constants.ts | 2 +- .../components/content/content.tsx | 443 +++++++++++++----- .../marketplace/components/header/header.scss | 15 +- .../load-more-button/load-more-button.tsx | 37 ++ .../components/product-card/product-card.tsx | 2 + .../product-list-content/no-results.tsx | 7 - .../components/product-list/types.ts | 2 + .../components/products/products.scss | 19 +- .../components/products/products.tsx | 71 +-- .../search-results/search-results.scss | 19 - .../search-results/search-results.tsx | 193 -------- .../marketplace/components/search/search.scss | 28 +- .../marketplace/components/search/search.tsx | 83 +--- .../marketplace/components/tabs/tabs.scss | 12 + .../marketplace/components/tabs/tabs.tsx | 130 ++--- .../contexts/marketplace-context.tsx | 33 +- .../client/marketplace/contexts/types.ts | 10 + .../client/marketplace/utils/functions.tsx | 25 +- .../client/marketplace/utils/tracking.ts | 10 - ...1309-update-21591-in-app-category-selector | 4 + ...update-wccom-21595-in-app-search-component | 4 + .../51342-fix-21596-search-loading-state | 4 + ...34-add-wccom-21568-in-app-load-more-button | 4 + .../changelog/refactor-wccom-21576-search-tab | 4 + .../tweak-21597-in-app-search-results-count | 4 + 27 files changed, 807 insertions(+), 616 deletions(-) create mode 100644 plugins/woocommerce-admin/client/marketplace/components/load-more-button/load-more-button.tsx delete mode 100644 plugins/woocommerce-admin/client/marketplace/components/search-results/search-results.scss delete mode 100644 plugins/woocommerce-admin/client/marketplace/components/search-results/search-results.tsx create mode 100644 plugins/woocommerce/changelog/51309-update-21591-in-app-category-selector create mode 100644 plugins/woocommerce/changelog/51313-update-wccom-21595-in-app-search-component create mode 100644 plugins/woocommerce/changelog/51342-fix-21596-search-loading-state create mode 100644 plugins/woocommerce/changelog/51434-add-wccom-21568-in-app-load-more-button create mode 100644 plugins/woocommerce/changelog/refactor-wccom-21576-search-tab create mode 100644 plugins/woocommerce/changelog/tweak-21597-in-app-search-results-count diff --git a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss index d713efc1a80..952780f05e2 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss +++ b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss @@ -1,13 +1,17 @@ @import "../../stylesheets/_variables.scss"; .woocommerce-marketplace__category-selector { + position: relative; display: flex; align-items: stretch; - margin: $grid-unit-20 0 0 0; + margin: 0; + overflow-x: auto; } .woocommerce-marketplace__category-item { cursor: pointer; + white-space: nowrap; + margin-bottom: 0; .components-dropdown { height: 100%; @@ -50,7 +54,6 @@ .woocommerce-marketplace__category-selector--full-width { display: none; - margin-top: $grid-unit-15; } @media screen and (max-width: $break-medium) { @@ -122,3 +125,22 @@ background-color: $gray-900; } } + +.woocommerce-marketplace__category-navigation-button { + border: none; + position: absolute; + top: 0; + bottom: 0; + height: 100%; + width: 50px; +} + +.woocommerce-marketplace__category-navigation-button--prev { + background: linear-gradient(to right, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); + left: 0; +} + +.woocommerce-marketplace__category-navigation-button--next { + background: linear-gradient(to left, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); + right: 0; +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx index 07b298cde66..9625aef5862 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx @@ -1,20 +1,21 @@ /** * External dependencies */ -import { useState, useEffect } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; +import { useState, useEffect, useRef } from '@wordpress/element'; import { useQuery } from '@woocommerce/navigation'; -import clsx from 'clsx'; +import { Icon } from '@wordpress/components'; +import { useDebounce } from '@wordpress/compose'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import CategoryLink from './category-link'; -import CategoryDropdown from './category-dropdown'; import { Category, CategoryAPIItem } from './types'; import { fetchCategories } from '../../utils/functions'; -import './category-selector.scss'; import { ProductType } from '../product-list/types'; +import CategoryDropdown from './category-dropdown'; +import './category-selector.scss'; const ALL_CATEGORIES_SLUGS = { [ ProductType.extension ]: '_all', @@ -29,32 +30,21 @@ interface CategorySelectorProps { export default function CategorySelector( props: CategorySelectorProps ): JSX.Element { - const [ visibleItems, setVisibleItems ] = useState< Category[] >( [] ); - const [ dropdownItems, setDropdownItems ] = useState< Category[] >( [] ); const [ selected, setSelected ] = useState< Category >(); const [ isLoading, setIsLoading ] = useState( false ); + const [ categoriesToShow, setCategoriesToShow ] = useState< Category[] >( + [] + ); + const [ isOverflowing, setIsOverflowing ] = useState( false ); + const [ scrollPosition, setScrollPosition ] = useState< + 'start' | 'middle' | 'end' + >( 'start' ); + + const categorySelectorRef = useRef< HTMLUListElement >( null ); + const selectedCategoryRef = useRef< HTMLLIElement >( null ); const query = useQuery(); - useEffect( () => { - // If no category is selected, show All as selected - let categoryToSearch = ALL_CATEGORIES_SLUGS[ props.type ]; - - if ( query.category ) { - categoryToSearch = query.category; - } - - const allCategories = visibleItems.concat( dropdownItems ); - - const selectedCategory = allCategories.find( - ( category ) => category.slug === categoryToSearch - ); - - if ( selectedCategory ) { - setSelected( selectedCategory ); - } - }, [ query.category, props.type, visibleItems, dropdownItems ] ); - useEffect( () => { setIsLoading( true ); @@ -72,21 +62,125 @@ export default function CategorySelector( return category.slug !== '_featured'; } ); - // Split array into two from 7th item - const visibleCategoryItems = categories.slice( 0, 7 ); - const dropdownCategoryItems = categories.slice( 7 ); - - setVisibleItems( visibleCategoryItems ); - setDropdownItems( dropdownCategoryItems ); + setCategoriesToShow( categories ); } ) .catch( () => { - setVisibleItems( [] ); - setDropdownItems( [] ); + setCategoriesToShow( [] ); } ) .finally( () => { setIsLoading( false ); } ); - }, [ props.type ] ); + }, [ props.type, setCategoriesToShow ] ); + + useEffect( () => { + // If no category is selected, show All as selected + let categoryToSearch = ALL_CATEGORIES_SLUGS[ props.type ]; + + if ( query.category ) { + categoryToSearch = query.category; + } + + const selectedCategory = categoriesToShow.find( + ( category ) => category.slug === categoryToSearch + ); + + if ( selectedCategory ) { + setSelected( selectedCategory ); + } + }, [ query.category, props.type, categoriesToShow ] ); + + useEffect( () => { + if ( selectedCategoryRef.current ) { + selectedCategoryRef.current.scrollIntoView( { + block: 'nearest', + inline: 'center', + } ); + } + }, [ selected ] ); + + function checkOverflow() { + if ( + categorySelectorRef.current && + categorySelectorRef.current.parentElement?.scrollWidth + ) { + const isContentOverflowing = + categorySelectorRef.current.scrollWidth > + categorySelectorRef.current.parentElement.scrollWidth; + + setIsOverflowing( isContentOverflowing ); + } + } + + function checkScrollPosition() { + const ulElement = categorySelectorRef.current; + + if ( ! ulElement ) { + return; + } + + const { scrollLeft, scrollWidth, clientWidth } = ulElement; + + if ( scrollLeft < 10 ) { + setScrollPosition( 'start' ); + + return; + } + + if ( scrollLeft + clientWidth < scrollWidth ) { + setScrollPosition( 'middle' ); + + return; + } + + if ( scrollLeft + clientWidth === scrollWidth ) { + setScrollPosition( 'end' ); + } + } + + const debouncedCheckOverflow = useDebounce( checkOverflow, 300 ); + const debouncedScrollPosition = useDebounce( checkScrollPosition, 100 ); + + function scrollCategories( scrollAmount: number ) { + if ( categorySelectorRef.current ) { + categorySelectorRef.current.scrollTo( { + left: categorySelectorRef.current.scrollLeft + scrollAmount, + behavior: 'smooth', + } ); + } + } + + function scrollToNextCategories() { + scrollCategories( 200 ); + } + + function scrollToPrevCategories() { + scrollCategories( -200 ); + } + + useEffect( () => { + window.addEventListener( 'resize', debouncedCheckOverflow ); + + const ulElement = categorySelectorRef.current; + + if ( ulElement ) { + ulElement.addEventListener( 'scroll', debouncedScrollPosition ); + } + + return () => { + window.removeEventListener( 'resize', debouncedCheckOverflow ); + + if ( ulElement ) { + ulElement.removeEventListener( + 'scroll', + debouncedScrollPosition + ); + } + }; + }, [ debouncedCheckOverflow, debouncedScrollPosition ] ); + + useEffect( () => { + checkOverflow(); + }, [ categoriesToShow ] ); function mobileCategoryDropdownLabel() { const allCategoriesText = __( 'All Categories', 'woocommerce' ); @@ -102,16 +196,6 @@ export default function CategorySelector( return selected.label; } - function isSelectedInDropdown() { - if ( ! selected ) { - return false; - } - - return dropdownItems.find( - ( category ) => category.slug === selected.slug - ); - } - if ( isLoading ) { return ( <> @@ -131,50 +215,62 @@ export default function CategorySelector( return ( <> -
    - { visibleItems.map( ( category ) => ( +
      + { categoriesToShow.map( ( category ) => (
    • ) ) } -
    • - { dropdownItems.length > 0 && ( - - ) } -
    -
    + { isOverflowing && ( + <> + + + + ) } ); } diff --git a/plugins/woocommerce-admin/client/marketplace/components/constants.ts b/plugins/woocommerce-admin/client/marketplace/components/constants.ts index de76eded4e2..d9b7febe49a 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/constants.ts +++ b/plugins/woocommerce-admin/client/marketplace/components/constants.ts @@ -10,7 +10,7 @@ export const MARKETPLACE_SEARCH_API_PATH = '/wp-json/wccom-extensions/1.0/search'; export const MARKETPLACE_CATEGORY_API_PATH = '/wp-json/wccom-extensions/1.0/categories'; -export const MARKETPLACE_ITEMS_PER_PAGE = 60; +export const MARKETPLACE_ITEMS_PER_PAGE = 60; // This should match the number of results returned by the API export const MARKETPLACE_SEARCH_RESULTS_PER_PAGE = 8; export const MARKETPLACE_CART_PATH = MARKETPLACE_HOST + '/cart/'; export const MARKETPLACE_RENEW_SUBSCRIPTON_PATH = diff --git a/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx b/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx index 1a4679dc4c1..06f1a17825b 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx @@ -1,22 +1,29 @@ /** * External dependencies */ -import { useContext, useEffect, useState } from '@wordpress/element'; +import { + useContext, + useEffect, + useState, + useCallback, +} from '@wordpress/element'; import { useQuery } from '@woocommerce/navigation'; +import { speak } from '@wordpress/a11y'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ import './content.scss'; -import { Product, ProductType, SearchResultType } from '../product-list/types'; +import { Product, ProductType } from '../product-list/types'; import { getAdminSetting } from '~/utils/admin-settings'; import Discover from '../discover/discover'; import Products from '../products/products'; -import SearchResults from '../search-results/search-results'; import MySubscriptions from '../my-subscriptions/my-subscriptions'; import { MarketplaceContext } from '../../contexts/marketplace-context'; -import { fetchSearchResults } from '../../utils/functions'; +import { fetchSearchResults, getProductType } from '../../utils/functions'; import { SubscriptionsContextProvider } from '../../contexts/subscriptions-context'; +import { SearchResultsCountType } from '../../contexts/types'; import { recordMarketplaceView, recordLegacyTabView, @@ -26,149 +33,350 @@ import Promotions from '../promotions/promotions'; import ConnectNotice from '~/marketplace/components/connect-notice/connect-notice'; import PluginInstallNotice from '../woo-update-manager-plugin/plugin-install-notice'; import SubscriptionsExpiredExpiringNotice from '~/marketplace/components/my-subscriptions/subscriptions-expired-expiring-notice'; +import LoadMoreButton from '../load-more-button/load-more-button'; export default function Content(): JSX.Element { const marketplaceContextValue = useContext( MarketplaceContext ); - const [ products, setProducts ] = useState< Product[] >( [] ); - const { setIsLoading, selectedTab, setHasBusinessServices } = - marketplaceContextValue; + const [ allProducts, setAllProducts ] = useState< Product[] >( [] ); + const [ filteredProducts, setFilteredProducts ] = useState< Product[] >( + [] + ); + const [ currentPage, setCurrentPage ] = useState( 1 ); + const [ totalPagesCategory, setTotalPagesCategory ] = useState( 1 ); + const [ totalPagesExtensions, setTotalPagesExtensions ] = useState( 1 ); + const [ totalPagesThemes, setTotalPagesThemes ] = useState( 1 ); + const [ totalPagesBusinessServices, setTotalPagesBusinessServices ] = + useState( 1 ); + const [ firstNewProductId, setFirstNewProductId ] = useState< number >( 0 ); + const [ isLoadingMore, setIsLoadingMore ] = useState( false ); + + const { + isLoading, + setIsLoading, + selectedTab, + setHasBusinessServices, + setSearchResultsCount, + } = marketplaceContextValue; const query = useQuery(); - // On initial load of the in-app marketplace, fetch extensions, themes and business services - // and check if there are any business services available on WCCOM - useEffect( () => { - const categories = [ '', 'themes', 'business-services' ]; - const abortControllers = categories.map( () => new AbortController() ); + const searchCompleteAnnouncement = ( count: number ): void => { + speak( + sprintf( + // translators: %d is the number of products found. + __( '%d products found', 'woocommerce' ), + count + ) + ); + }; - categories.forEach( ( category: string, index ) => { - const params = new URLSearchParams(); - if ( category !== '' ) { - params.append( 'category', category ); - } + const tagProductsWithType = ( + products: Product[], + type: ProductType + ): Product[] => { + return products.map( ( product ) => ( { + ...product, + type, + } ) ); + }; - const wccomSettings = getAdminSetting( 'wccomHelper', false ); - if ( wccomSettings.storeCountry ) { - params.append( 'country', wccomSettings.storeCountry ); - } - - fetchSearchResults( params, abortControllers[ index ].signal ).then( - ( productList ) => { - if ( category === 'business-services' ) { - setHasBusinessServices( productList.length > 0 ); - } - } - ); - return () => { - abortControllers.forEach( ( controller ) => { - controller.abort(); - } ); - }; - } ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [] ); - - // Get the content for this screen - useEffect( () => { + const loadMoreProducts = useCallback( () => { + setIsLoadingMore( true ); + const params = new URLSearchParams(); const abortController = new AbortController(); - if ( - query.tab === undefined || - ( query.tab && - [ '', 'discover', 'my-subscriptions' ].includes( query.tab ) ) - ) { - return; + if ( query.category && query.category !== '_all' ) { + params.append( 'category', query.category ); } - setIsLoading( true ); - setProducts( [] ); - - const params = new URLSearchParams(); + if ( query.tab === 'themes' || query.tab === 'business-services' ) { + params.append( 'category', query.tab ); + } if ( query.term ) { params.append( 'term', query.term ); } - if ( query.category ) { - params.append( - 'category', - query.category === '_all' ? '' : query.category - ); - } else if ( query?.tab === 'themes' ) { - params.append( 'category', 'themes' ); - } else if ( query?.tab === 'business-services' ) { - params.append( 'category', 'business-services' ); - } else if ( query?.tab === 'search' ) { - params.append( 'category', 'extensions-themes-business-services' ); - } - const wccomSettings = getAdminSetting( 'wccomHelper', false ); if ( wccomSettings.storeCountry ) { params.append( 'country', wccomSettings.storeCountry ); } + params.append( 'page', ( currentPage + 1 ).toString() ); + fetchSearchResults( params, abortController.signal ) .then( ( productList ) => { - setProducts( productList ); + setAllProducts( ( prevProducts ) => { + const flattenedPrevProducts = Array.isArray( + prevProducts[ 0 ] + ) + ? prevProducts.flat() + : prevProducts; + + const newProducts = productList.products.filter( + ( newProduct ) => + ! flattenedPrevProducts.some( + ( prevProduct ) => + prevProduct.id === newProduct.id + ) + ); + + if ( newProducts.length > 0 ) { + setFirstNewProductId( newProducts[ 0 ].id ?? 0 ); + } + + const combinedProducts = [ + ...flattenedPrevProducts, + ...newProducts, + ]; + + return combinedProducts; + } ); + + speak( __( 'More products loaded', 'woocommerce' ) ); + setCurrentPage( ( prevPage ) => prevPage + 1 ); + setIsLoadingMore( false ); } ) .catch( () => { - setProducts( [] ); + speak( __( 'Error loading more products', 'woocommerce' ) ); } ) .finally( () => { - // we are recording both the new and legacy events here for now - // they're separate methods to make it easier to remove the legacy one later - const marketplaceViewProps = { - view: query?.tab, - search_term: query?.term, - product_type: query?.section, - category: query?.category, - }; - - recordMarketplaceView( marketplaceViewProps ); - recordLegacyTabView( marketplaceViewProps ); - setIsLoading( false ); + setIsLoadingMore( false ); } ); + return () => { abortController.abort(); }; }, [ + currentPage, + query.category, + query.term, + query.tab, + setIsLoadingMore, + ] ); + + useEffect( () => { + // if it's a paginated request, don't use this effect + if ( currentPage > 1 ) { + return; + } + + const categories: Array< { + category: keyof SearchResultsCountType; + type: ProductType; + } > = [ + { category: 'extensions', type: ProductType.extension }, + { category: 'themes', type: ProductType.theme }, + { + category: 'business-services', + type: ProductType.businessService, + }, + ]; + const abortControllers = categories.map( () => new AbortController() ); + + setIsLoading( true ); + setAllProducts( [] ); + + // If query.category is present and not '_all', only fetch that category + if ( query.category && query.category !== '_all' ) { + const params = new URLSearchParams(); + + params.append( 'category', query.category ); + + if ( query.term ) { + params.append( 'term', query.term ); + } + + const wccomSettings = getAdminSetting( 'wccomHelper', false ); + if ( wccomSettings.storeCountry ) { + params.append( 'country', wccomSettings.storeCountry ); + } + + fetchSearchResults( params, abortControllers[ 0 ].signal ) + .then( ( productList ) => { + setAllProducts( productList.products ); + setTotalPagesCategory( productList.totalPages ); + setSearchResultsCount( { + [ query.tab ]: productList.totalProducts, + } ); + + searchCompleteAnnouncement( productList.totalProducts ); + } ) + .catch( () => { + setAllProducts( [] ); + } ) + .finally( () => { + setIsLoading( false ); + } ); + } else { + // Fetch all tabs when query.term or query.category changes + Promise.all( + categories.map( ( { category, type }, index ) => { + const params = new URLSearchParams(); + if ( category !== 'extensions' ) { + params.append( 'category', category ); + } + if ( query.term ) { + params.append( 'term', query.term ); + } + + const wccomSettings = getAdminSetting( + 'wccomHelper', + false + ); + if ( wccomSettings.storeCountry ) { + params.append( 'country', wccomSettings.storeCountry ); + } + + return fetchSearchResults( + params, + abortControllers[ index ].signal + ).then( ( productList ) => { + const typedProducts = tagProductsWithType( + productList.products, + type + ); + if ( category === 'business-services' ) { + setHasBusinessServices( typedProducts.length > 0 ); + } + return { + products: typedProducts, + totalPages: productList.totalPages, + totalProducts: productList.totalProducts, + type, + }; + } ); + } ) + ) + .then( ( results ) => { + const combinedProducts = results.flatMap( + ( result ) => result.products + ); + + setAllProducts( combinedProducts ); + + setSearchResultsCount( { + extensions: results.find( + ( i ) => i.type === 'extension' + )?.totalProducts, + themes: results.find( ( i ) => i.type === 'theme' ) + ?.totalProducts, + 'business-services': results.find( + ( i ) => i.type === 'business-service' + )?.totalProducts, + } ); + + results.forEach( ( result ) => { + switch ( result.type ) { + case ProductType.extension: + setTotalPagesExtensions( result.totalPages ); + break; + case ProductType.theme: + setTotalPagesThemes( result.totalPages ); + break; + case ProductType.businessService: + setTotalPagesBusinessServices( + result.totalPages + ); + break; + } + } ); + + searchCompleteAnnouncement( + results.reduce( ( acc, curr ) => { + return acc + curr.totalProducts; + }, 0 ) + ); + } ) + .catch( () => { + setAllProducts( [] ); + } ) + .finally( () => { + setIsLoading( false ); + } ); + } + + return () => { + abortControllers.forEach( ( controller ) => { + controller.abort(); + } ); + }; + }, [ + query.tab, query.term, query.category, - query?.tab, + setHasBusinessServices, setIsLoading, - query?.section, + setSearchResultsCount, + currentPage, ] ); + // Filter the products based on the selected tab + useEffect( () => { + let filtered: Product[] | null; + switch ( selectedTab ) { + case 'extensions': + filtered = allProducts.filter( + ( p ) => p.type === ProductType.extension + ); + break; + case 'themes': + filtered = allProducts.filter( + ( p ) => p.type === ProductType.theme + ); + break; + case 'business-services': + filtered = allProducts.filter( + ( p ) => p.type === ProductType.businessService + ); + break; + default: + filtered = []; + } + setFilteredProducts( filtered ); + }, [ selectedTab, allProducts ] ); + + // Record tab view events when the query changes + useEffect( () => { + const marketplaceViewProps = { + view: query?.tab, + search_term: query?.term, + product_type: query?.section, + category: query?.category, + }; + recordMarketplaceView( marketplaceViewProps ); + recordLegacyTabView( marketplaceViewProps ); + }, [ query?.tab, query?.term, query?.section, query?.category ] ); + + // Reset current page when tab, term, or category changes + useEffect( () => { + setCurrentPage( 1 ); + setFirstNewProductId( 0 ); + }, [ selectedTab, query?.category, query?.term ] ); + + // Maintain product focus for accessibility + useEffect( () => { + if ( firstNewProductId ) { + setTimeout( () => { + const firstNewProduct = document.getElementById( + `product-${ firstNewProductId }` + ); + if ( firstNewProduct ) { + firstNewProduct.focus(); + } + }, 0 ); + } + }, [ firstNewProductId ] ); + const renderContent = (): JSX.Element => { switch ( selectedTab ) { case 'extensions': - return ( - - ); case 'themes': - return ( - - ); case 'business-services': return ( - ); - case 'search': - return ( - ); case 'discover': @@ -184,10 +392,29 @@ export default function Content(): JSX.Element { } }; + const shouldShowLoadMoreButton = () => { + if ( ! query.category || query.category === '_all' ) { + // Check against total pages for the selected tab + switch ( selectedTab ) { + case 'extensions': + return currentPage < totalPagesExtensions; + case 'themes': + return currentPage < totalPagesThemes; + case 'business-services': + return currentPage < totalPagesBusinessServices; + default: + return false; + } + } else { + // Check against totalPagesCategory for specific category + return currentPage < totalPagesCategory; + } + }; + return (
    - + { selectedTab !== 'business-services' && selectedTab !== 'my-subscriptions' && } { selectedTab !== 'business-services' && } @@ -197,11 +424,15 @@ export default function Content(): JSX.Element { { selectedTab !== 'business-services' && ( ) } - { selectedTab !== 'business-services' && ( - - ) } { renderContent() } + { ! isLoading && shouldShowLoadMoreButton() && ( + + ) }
    ); } diff --git a/plugins/woocommerce-admin/client/marketplace/components/header/header.scss b/plugins/woocommerce-admin/client/marketplace/components/header/header.scss index 11616f62a7f..f4e14f0ff23 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/header/header.scss +++ b/plugins/woocommerce-admin/client/marketplace/components/header/header.scss @@ -5,6 +5,7 @@ background: #fff; border-bottom: 1px solid $gutenberg-gray-300; display: grid; + gap: $medium-gap; grid-template: "mktpl-title mktpl-search mktpl-meta" 60px "mktpl-tabs mktpl-tabs mktpl-tabs" auto / 1fr 320px 36px; padding: 0 $content-spacing-large; @@ -73,17 +74,3 @@ padding: 0 $content-spacing-small; } } - -.woocommerce-marketplace__search { - margin-right: $medium-gap; - margin-top: 10px; - - input[type="search"] { - all: unset; - flex-grow: 1; - } - - @media (width <= $breakpoint-medium) { - margin: $content-spacing-small; - } -} diff --git a/plugins/woocommerce-admin/client/marketplace/components/load-more-button/load-more-button.tsx b/plugins/woocommerce-admin/client/marketplace/components/load-more-button/load-more-button.tsx new file mode 100644 index 00000000000..07fb2ea6c48 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketplace/components/load-more-button/load-more-button.tsx @@ -0,0 +1,37 @@ +/** + * External dependencies + */ +import { Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { speak } from '@wordpress/a11y'; +import { queueRecordEvent } from '@woocommerce/tracks'; + +interface LoadMoreProps { + onLoadMore: () => void; + isBusy: boolean; + disabled: boolean; +} + +export default function LoadMoreButton( props: LoadMoreProps ) { + const { onLoadMore, isBusy, disabled } = props; + function handleClick() { + queueRecordEvent( 'marketplace_load_more_button_clicked', {} ); + onLoadMore(); + } + + if ( isBusy ) { + speak( __( 'Loading more products', 'woocommerce' ) ); + } + + return ( + + ); +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.tsx b/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.tsx index 79585e83189..00ba4a2d0e7 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.tsx @@ -191,6 +191,8 @@ function ProductCard( props: ProductCardProps ): JSX.Element { return ( diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-list-content/no-results.tsx b/plugins/woocommerce-admin/client/marketplace/components/product-list-content/no-results.tsx index da81e0d8059..9ecdd54c98a 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/product-list-content/no-results.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/product-list-content/no-results.tsx @@ -3,7 +3,6 @@ */ import { __ } from '@wordpress/i18n'; import { useEffect, useState } from '@wordpress/element'; -import { useQuery } from '@woocommerce/navigation'; /** * Internal dependencies @@ -22,8 +21,6 @@ export default function NoResults( props: { } ): JSX.Element { const [ productGroups, setProductGroups ] = useState< ProductGroup[] >(); const [ isLoading, setIsLoading ] = useState( false ); - const query = useQuery(); - const showCategorySelector = query.tab === 'search' && query.section; const productGroupsForSearchType = { [ SearchResultType.all ]: [ 'most-popular', @@ -123,10 +120,6 @@ export default function NoResults( props: { } function categorySelector() { - if ( ! showCategorySelector ) { - return <>; - } - if ( props.type === SearchResultType.all ) { return <>; } diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts b/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts index 5008cef836a..9ee660ddb08 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts +++ b/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts @@ -1,5 +1,7 @@ export type SearchAPIJSONType = { products: Array< SearchAPIProductType >; + total_pages: number; + total_products: number; }; export type SearchAPIProductType = { diff --git a/plugins/woocommerce-admin/client/marketplace/components/products/products.scss b/plugins/woocommerce-admin/client/marketplace/components/products/products.scss index 2d92bfd5259..4fb5f89218c 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/products/products.scss +++ b/plugins/woocommerce-admin/client/marketplace/components/products/products.scss @@ -9,10 +9,21 @@ } } + .woocommerce-marketplace__sub-header { display: flex; - - .woocommerce-marketplace__customize-your-store-button { - margin: 16px 0 6px auto; - } + align-items: center; + justify-content: space-between; + gap: 32px; } + +.woocommerce-marketplace__sub-header__categories { + flex: 1; + overflow-x: auto; + position: relative; +} + +.woocommerce-marketplace__customize-your-store-button { + flex-shrink: 0; +} + diff --git a/plugins/woocommerce-admin/client/marketplace/components/products/products.tsx b/plugins/woocommerce-admin/client/marketplace/components/products/products.tsx index 8ecd211e4d2..a078e991198 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/products/products.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/products/products.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { __, _n, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import { createInterpolateElement, useContext, @@ -24,7 +24,6 @@ import ProductListContent from '../product-list-content/product-list-content'; import ProductLoader from '../product-loader/product-loader'; import NoResults from '../product-list-content/no-results'; import { Product, ProductType, SearchResultType } from '../product-list/types'; -import { MARKETPLACE_ITEMS_PER_PAGE } from '../constants'; import { ADMIN_URL } from '~/utils/admin-settings'; import { ThemeSwitchWarningModal } from '~/customize-store/intro/warning-modals'; @@ -54,12 +53,10 @@ const LABELS = { export default function Products( props: ProductsProps ) { const marketplaceContextValue = useContext( MarketplaceContext ); - const { isLoading, selectedTab } = marketplaceContextValue; + const { isLoading } = marketplaceContextValue; const label = LABELS[ props.type ].label; - const singularLabel = LABELS[ props.type ].singularLabel; const query = useQuery(); const category = query?.category; - const perPage = props.perPage ?? MARKETPLACE_ITEMS_PER_PAGE; interface Theme { stylesheet?: string; } @@ -94,42 +91,30 @@ export default function Products( props: ProductsProps ) { } // Store the total number of products before we slice it later. - const productTotalCount = props.products?.length ?? 0; - const products = props.products?.slice( 0, perPage ) ?? []; - - let title = sprintf( - // translators: %s: plural item type (e.g. extensions, themes) - __( '0 %s found', 'woocommerce' ), - label - ); - - if ( productTotalCount > 0 ) { - title = sprintf( - // translators: %1$s: number of items, %2$s: singular item label, %3$s: plural item label - _n( '%1$s %2$s', '%1$s %3$s', productTotalCount, 'woocommerce' ), - productTotalCount, - singularLabel, - label - ); - } + const products = props.products ?? []; const labelForClassName = label === 'business services' ? 'business-services' : label; const baseContainerClass = 'woocommerce-marketplace__search-'; - const baseProductListTitleClass = 'product-list-title--'; const containerClassName = clsx( baseContainerClass + labelForClassName ); - const productListTitleClassName = clsx( - 'woocommerce-marketplace__product-list-title', - baseContainerClass + baseProductListTitleClass + labelForClassName, - { 'is-loading': isLoading } - ); const viewAllButonClassName = clsx( 'woocommerce-marketplace__view-all-button', baseContainerClass + 'button-' + labelForClassName ); + if ( isLoading ) { + return ( + <> + { props.categorySelector && ( + + ) } + + + ); + } + if ( products.length === 0 ) { let type = SearchResultType.all; @@ -154,28 +139,14 @@ export default function Products( props: ProductsProps ) { : '' ); - if ( isLoading ) { - return ( - <> - { props.categorySelector && ( - - ) } - - - ); - } - return (
    - { selectedTab === 'search' && ( -

    - { isLoading ? ' ' : title } -

    - ) } -
    - { props.categorySelector && ( - - ) } +
    + { isModalOpen && ( product.type === ProductType.extension - ); - const themeList = props.products.filter( - ( product ) => product.type === ProductType.theme - ); - const businessServiceList = props.products.filter( - ( product ) => product.type === ProductType.businessService - ); - - const hasExtensions = extensionList.length > 0; - const hasThemes = themeList.length > 0; - const hasBusinessServices = businessServiceList.length > 0; - const hasOnlyExtensions = - hasExtensions && ! hasThemes && ! hasBusinessServices; - const hasOnlyThemes = hasThemes && ! hasExtensions && ! hasBusinessServices; - const hasOnlyBusinessServices = - hasBusinessServices && ! hasExtensions && ! hasThemes; - - const marketplaceContextValue = useContext( MarketplaceContext ); - const { isLoading, hasBusinessServices: canShowBusinessServices } = - marketplaceContextValue; - - const query = useQuery(); - const showCategorySelector = query.section ? true : false; - const searchTerm = query.term ? query.term : ''; - - type Overrides = { - categorySelector?: boolean; - showAllButton?: boolean; - perPage?: number; - }; - - function productsComponent( - products: Product[], - type: ProductType, - overrides: Overrides = {} - ) { - return ( - - ); - } - - function extensionsComponent( overrides: Overrides = {} ) { - return productsComponent( - extensionList, - ProductType.extension, - overrides - ); - } - - function themesComponent( overrides: Overrides = {} ) { - return productsComponent( themeList, ProductType.theme, overrides ); - } - - function businessServicesComponent( overrides: Overrides = {} ) { - return productsComponent( - businessServiceList, - ProductType.businessService, - overrides - ); - } - - const content = () => { - if ( query?.section === SearchResultType.extension ) { - return extensionsComponent( { showAllButton: false } ); - } - - if ( query?.section === SearchResultType.theme ) { - return themesComponent( { showAllButton: false } ); - } - - if ( query?.section === SearchResultType.businessService ) { - return businessServicesComponent( { showAllButton: false } ); - } - - // Components can handle their isLoading state. So we can put all three on the page. - if ( isLoading ) { - return ( - <> - { extensionsComponent() } - { themesComponent() } - { businessServicesComponent() } - - ); - } - - // If we did finish loading items, and there are no results, show the no results component. - if ( - ! isLoading && - ! hasExtensions && - ! hasThemes && - ! hasBusinessServices - ) { - return ( - - ); - } - - // If we're done loading, we can put these components on the page. - return ( - <> - { hasExtensions - ? extensionsComponent( { - categorySelector: hasOnlyExtensions || undefined, - showAllButton: hasOnlyExtensions - ? false - : undefined, - perPage: hasOnlyExtensions - ? MARKETPLACE_ITEMS_PER_PAGE - : MARKETPLACE_SEARCH_RESULTS_PER_PAGE, - } ) - : null } - { hasThemes - ? themesComponent( { - categorySelector: hasOnlyThemes || undefined, - showAllButton: hasOnlyThemes ? false : undefined, - perPage: hasOnlyThemes - ? MARKETPLACE_ITEMS_PER_PAGE - : MARKETPLACE_SEARCH_RESULTS_PER_PAGE, - } ) - : null } - { hasBusinessServices - ? businessServicesComponent( { - categorySelector: - hasOnlyBusinessServices || undefined, - showAllButton: hasOnlyBusinessServices - ? false - : undefined, - perPage: hasOnlyBusinessServices - ? MARKETPLACE_ITEMS_PER_PAGE - : MARKETPLACE_SEARCH_RESULTS_PER_PAGE, - } ) - : null } - - ); - }; - - return ( -
    - { content() } -
    - ); -} diff --git a/plugins/woocommerce-admin/client/marketplace/components/search/search.scss b/plugins/woocommerce-admin/client/marketplace/components/search/search.scss index e6725b7ca2d..0fb878f214f 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/search/search.scss +++ b/plugins/woocommerce-admin/client/marketplace/components/search/search.scss @@ -2,29 +2,15 @@ .woocommerce-marketplace__search { grid-area: mktpl-search; - background: $gutenberg-gray-100; - border: 1.5px solid transparent; - border-radius: 2px; - display: flex; - height: 40px; - padding: 4px 8px 4px 12px; - - input[type="search"] { - all: unset; - flex-grow: 1; - } - - &:focus-within { - background: #fff; - border-color: var(--wp-admin-theme-color, #3858e9); - } + margin-top: 15px; + width: 320px; @media (width <= $breakpoint-medium) { margin: $grid-unit-20 $grid-unit-20 $grid-unit-10 $grid-unit-20; + width: calc(100% - $grid-unit-20 * 2); + + .components-input-control__input { + font-size: 13px !important; + } } } - -.woocommerce-marketplace__search-button { - all: unset; - cursor: pointer; -} diff --git a/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx b/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx index e03582ca4e8..f9572c7c7fa 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx @@ -2,26 +2,20 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { Icon, search } from '@wordpress/icons'; -import { useContext, useEffect, useState } from '@wordpress/element'; +import { useEffect, useState } from '@wordpress/element'; import { navigateTo, getNewPath, useQuery } from '@woocommerce/navigation'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +// eslint-disable-next-line @woocommerce/dependency-group +import { SearchControl } from '@wordpress/components'; +// The @ts-ignore is needed because the SearchControl types are not exported from the @wordpress/components package, +// even though the component itself is. This is likely due to an older version of the package being used. /** * Internal dependencies */ import './search.scss'; import { MARKETPLACE_PATH } from '../constants'; -import { MarketplaceContext } from '../../contexts/marketplace-context'; - -const searchPlaceholder = __( - 'Search for extensions, themes, and business services', - 'woocommerce' -); - -const searchPlaceholderNoBusinessServices = __( - 'Search for extensions and themes', - 'woocommerce' -); /** * Search component. @@ -30,14 +24,10 @@ const searchPlaceholderNoBusinessServices = __( */ function Search(): JSX.Element { const [ searchTerm, setSearchTerm ] = useState( '' ); - const { hasBusinessServices } = useContext( MarketplaceContext ); + const searchPlaceholder = __( 'Search Marketplace', 'woocommerce' ); const query = useQuery(); - const placeholder = hasBusinessServices - ? searchPlaceholder - : searchPlaceholderNoBusinessServices; - useEffect( () => { if ( query.term ) { setSearchTerm( query.term ); @@ -46,21 +36,16 @@ function Search(): JSX.Element { } }, [ query.term ] ); - useEffect( () => { - if ( query.tab !== 'search' ) { - setSearchTerm( '' ); - } - }, [ query.tab ] ); - const runSearch = () => { - const term = searchTerm.trim(); + const newQuery: { term?: string; tab?: string } = query; - const newQuery: { term?: string; tab?: string } = {}; - if ( term !== '' ) { - newQuery.term = term; - newQuery.tab = 'search'; + // If we're on 'Discover' or 'My subscriptions' when a search is initiated, move to the extensions tab + if ( ! newQuery.tab || newQuery.tab === 'my-subscriptions' ) { + newQuery.tab = 'extensions'; } + newQuery.term = searchTerm.trim(); + // When the search term changes, we reset the query string on purpose. navigateTo( { url: getNewPath( newQuery, MARKETPLACE_PATH, {} ), @@ -69,12 +54,6 @@ function Search(): JSX.Element { return []; }; - const handleInputChange = ( - event: React.ChangeEvent< HTMLInputElement > - ) => { - setSearchTerm( event.target.value ); - }; - const handleKeyUp = ( event: { key: string } ) => { if ( event.key === 'Enter' ) { runSearch(); @@ -86,32 +65,14 @@ function Search(): JSX.Element { }; return ( -
    - - - -
    + ); } diff --git a/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss b/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss index 008fe4e1b84..3a8e7c152e6 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss +++ b/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss @@ -45,6 +45,18 @@ text-align: center; z-index: 26; } + + &__update-count-extensions, + &__update-count-themes, + &__update-count-business-services { + background-color: $gutenberg-gray-300; + color: $gutenberg-gray-700; + + &.is-active { + background-color: #000; + color: #fff; + } + } } @media (width <= $breakpoint-medium) { diff --git a/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.tsx b/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.tsx index 1f852a2b982..052a06df435 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { useContext, useEffect, useState } from '@wordpress/element'; +import { useContext, useEffect, useState, useMemo } from '@wordpress/element'; import { Button } from '@wordpress/components'; import clsx from 'clsx'; import { getNewPath, navigateTo, useQuery } from '@woocommerce/navigation'; @@ -35,63 +35,26 @@ interface Tabs { const wccomSettings = getAdminSetting( 'wccomHelper', {} ); const wooUpdateCount = wccomSettings?.wooUpdateCount ?? 0; -const tabs: Tabs = { - search: { - name: 'search', - title: __( 'Search results', 'woocommerce' ), - showUpdateCount: false, - updateCount: 0, - }, - discover: { - name: 'discover', - title: __( 'Discover', 'woocommerce' ), - showUpdateCount: false, - updateCount: 0, - }, - extensions: { - name: 'extensions', - title: __( 'Extensions', 'woocommerce' ), - showUpdateCount: false, - updateCount: 0, - }, - themes: { - name: 'themes', - title: __( 'Themes', 'woocommerce' ), - showUpdateCount: false, - updateCount: 0, - }, - 'business-services': { - name: 'business-services', - title: __( 'Business services', 'woocommerce' ), - showUpdateCount: false, - updateCount: 0, - }, - 'my-subscriptions': { - name: 'my-subscriptions', - title: __( 'My subscriptions', 'woocommerce' ), - showUpdateCount: true, - updateCount: wooUpdateCount, - }, -}; - -const setUrlTabParam = ( tabKey: string ) => { +const setUrlTabParam = ( tabKey: string, query: Record< string, string > ) => { + const term = query.term ? { term: query.term.trim() } : {}; navigateTo( { url: getNewPath( { tab: tabKey === DEFAULT_TAB_KEY ? undefined : tabKey }, MARKETPLACE_PATH, - {} + term ), } ); }; -const getVisibleTabs = ( selectedTab: string, hasBusinessServices = false ) => { +const getVisibleTabs = ( + selectedTab: string, + hasBusinessServices = false, + tabs: Tabs +) => { if ( selectedTab === '' ) { return tabs; } const currentVisibleTabs = { ...tabs }; - if ( selectedTab !== 'search' ) { - delete currentVisibleTabs.search; - } if ( ! hasBusinessServices ) { delete currentVisibleTabs[ 'business-services' ]; } @@ -101,7 +64,9 @@ const getVisibleTabs = ( selectedTab: string, hasBusinessServices = false ) => { const renderTabs = ( marketplaceContextValue: MarketplaceContextType, - visibleTabs: Tabs + visibleTabs: Tabs, + tabs: Tabs, + query: Record< string, string > ) => { const { selectedTab, setSelectedTab } = marketplaceContextValue; @@ -110,7 +75,7 @@ const renderTabs = ( return; } setSelectedTab( tabKey ); - setUrlTabParam( tabKey ); + setUrlTabParam( tabKey, query ); }; const tabContent = []; @@ -143,7 +108,15 @@ const renderTabs = ( { tabs[ tabKey ]?.title } { tabs[ tabKey ]?.showUpdateCount && tabs[ tabKey ]?.updateCount > 0 && ( - + { tabs[ tabKey ]?.updateCount } ) } @@ -157,23 +130,70 @@ const renderTabs = ( const Tabs = ( props: TabsProps ): JSX.Element => { const { additionalClassNames } = props; const marketplaceContextValue = useContext( MarketplaceContext ); - const { selectedTab, setSelectedTab, hasBusinessServices } = + const { selectedTab, isLoading, setSelectedTab, hasBusinessServices } = marketplaceContextValue; - const [ visibleTabs, setVisibleTabs ] = useState( getVisibleTabs( '' ) ); + const { searchResultsCount } = marketplaceContextValue; const query: Record< string, string > = useQuery(); + const tabs: Tabs = useMemo( + () => ( { + discover: { + name: 'discover', + title: __( 'Discover', 'woocommerce' ), + showUpdateCount: false, + updateCount: 0, + }, + extensions: { + name: 'extensions', + title: __( 'Extensions', 'woocommerce' ), + showUpdateCount: !! query.term && ! isLoading, + updateCount: searchResultsCount.extensions, + }, + themes: { + name: 'themes', + title: __( 'Themes', 'woocommerce' ), + showUpdateCount: !! query.term && ! isLoading, + updateCount: searchResultsCount.themes, + }, + 'business-services': { + name: 'business-services', + title: __( 'Business services', 'woocommerce' ), + showUpdateCount: !! query.term && ! isLoading, + updateCount: searchResultsCount[ 'business-services' ], + }, + 'my-subscriptions': { + name: 'my-subscriptions', + title: __( 'My subscriptions', 'woocommerce' ), + showUpdateCount: true, + updateCount: wooUpdateCount, + }, + } ), + [ query, isLoading, searchResultsCount ] + ); + + const [ visibleTabs, setVisibleTabs ] = useState( + getVisibleTabs( '', false, tabs ) + ); + useEffect( () => { if ( query?.tab && tabs[ query.tab ] ) { setSelectedTab( query.tab ); } else if ( Object.keys( query ).length > 0 ) { setSelectedTab( DEFAULT_TAB_KEY ); } - }, [ query, setSelectedTab ] ); + }, [ query, setSelectedTab, tabs ] ); useEffect( () => { - setVisibleTabs( getVisibleTabs( selectedTab, hasBusinessServices ) ); - }, [ selectedTab, hasBusinessServices ] ); + setVisibleTabs( + getVisibleTabs( selectedTab, hasBusinessServices, tabs ) + ); + + if ( selectedTab === 'business-services' && ! hasBusinessServices ) { + setUrlTabParam( 'extensions', query ); + } + }, [ selectedTab, hasBusinessServices, query, tabs ] ); + return ( ); }; diff --git a/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx b/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx index 53dd1bb5789..0631bf5725e 100644 --- a/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx +++ b/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx @@ -1,12 +1,17 @@ /** * External dependencies */ -import { useState, useEffect, createContext } from '@wordpress/element'; +import { + useState, + useEffect, + useCallback, + createContext, +} from '@wordpress/element'; /** * Internal dependencies */ -import { MarketplaceContextType } from './types'; +import { SearchResultsCountType, MarketplaceContextType } from './types'; import { getAdminSetting } from '../../utils/admin-settings'; export const MarketplaceContext = createContext< MarketplaceContextType >( { @@ -18,6 +23,12 @@ export const MarketplaceContext = createContext< MarketplaceContextType >( { addInstalledProduct: () => {}, hasBusinessServices: false, setHasBusinessServices: () => {}, + searchResultsCount: { + extensions: 0, + themes: 0, + 'business-services': 0, + }, + setSearchResultsCount: () => {}, } ); export function MarketplaceContextProvider( props: { @@ -29,6 +40,22 @@ export function MarketplaceContextProvider( props: { [] ); const [ hasBusinessServices, setHasBusinessServices ] = useState( false ); + const [ searchResultsCount, setSearchResultsCountState ] = + useState< SearchResultsCountType >( { + extensions: 0, + themes: 0, + 'business-services': 0, + } ); + + const setSearchResultsCount = useCallback( + ( updatedCounts: Partial< SearchResultsCountType > ) => { + setSearchResultsCountState( ( prev ) => ( { + ...prev, + ...updatedCounts, + } ) ); + }, + [] + ); /** * Knowing installed products will help us to determine which products @@ -59,6 +86,8 @@ export function MarketplaceContextProvider( props: { addInstalledProduct, hasBusinessServices, setHasBusinessServices, + searchResultsCount, + setSearchResultsCount, }; return ( diff --git a/plugins/woocommerce-admin/client/marketplace/contexts/types.ts b/plugins/woocommerce-admin/client/marketplace/contexts/types.ts index 969ce88d2e1..3240729871e 100644 --- a/plugins/woocommerce-admin/client/marketplace/contexts/types.ts +++ b/plugins/woocommerce-admin/client/marketplace/contexts/types.ts @@ -8,6 +8,12 @@ import { Options } from '@wordpress/notices'; */ import { Subscription } from '../components/my-subscriptions/types'; +export interface SearchResultsCountType { + extensions: number; + themes: number; + 'business-services': number; +} + export type MarketplaceContextType = { isLoading: boolean; setIsLoading: ( isLoading: boolean ) => void; @@ -17,6 +23,10 @@ export type MarketplaceContextType = { addInstalledProduct: ( slug: string ) => void; hasBusinessServices: boolean; setHasBusinessServices: ( hasBusinessServices: boolean ) => void; + searchResultsCount: SearchResultsCountType; + setSearchResultsCount: ( + updatedCounts: Partial< SearchResultsCountType > + ) => void; }; export type SubscriptionsContextType = { diff --git a/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx b/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx index d7fbc982c15..a063490eefc 100644 --- a/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx +++ b/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx @@ -107,7 +107,11 @@ async function fetchJsonWithCache( async function fetchSearchResults( params: URLSearchParams, abortSignal?: AbortSignal -): Promise< Product[] > { +): Promise< { + products: Product[]; + totalPages: number; + totalProducts: number; +} > { const url = MARKETPLACE_HOST + MARKETPLACE_SEARCH_API_PATH + @@ -151,9 +155,12 @@ async function fetchSearchResults( }; } ); - resolve( products ); + const totalPages = ( json as SearchAPIJSONType ).total_pages; + const totalProducts = ( json as SearchAPIJSONType ) + .total_products; + resolve( { products, totalPages, totalProducts } ); } ) - .catch( () => reject ); + .catch( reject ); } ); } @@ -174,6 +181,17 @@ async function fetchDiscoverPageData(): Promise< ProductGroup[] > { } } +function getProductType( tab: string ): ProductType { + switch ( tab ) { + case 'themes': + return ProductType.theme; + case 'business-services': + return ProductType.businessService; + default: + return ProductType.extension; + } +} + function fetchCategories( type: ProductType ): Promise< CategoryAPIItem[] > { const url = new URL( MARKETPLACE_HOST + MARKETPLACE_CATEGORY_API_PATH ); @@ -478,6 +496,7 @@ export { fetchCategories, fetchDiscoverPageData, fetchSearchResults, + getProductType, fetchSubscriptions, refreshSubscriptions, getInstallUrl, diff --git a/plugins/woocommerce-admin/client/marketplace/utils/tracking.ts b/plugins/woocommerce-admin/client/marketplace/utils/tracking.ts index 6729c824c55..8b08027770e 100644 --- a/plugins/woocommerce-admin/client/marketplace/utils/tracking.ts +++ b/plugins/woocommerce-admin/client/marketplace/utils/tracking.ts @@ -42,11 +42,6 @@ function recordMarketplaceView( props: MarketplaceViewProps ) { eventProps.category = '_all'; } - // User clicks the `View All` button on search results - if ( view && view === 'search' && product_type && ! category ) { - eventProps.category = '_all'; - } - recordEvent( 'marketplace_view', eventProps ); } @@ -80,11 +75,6 @@ function recordLegacyTabView( props: MarketplaceViewProps ) { case 'themes': oldEventProps.section = 'themes'; break; - case 'search': - oldEventName = 'extensions_view_search'; - oldEventProps.section = view; - oldEventProps.search_term = search_term || ''; - break; case 'my-subscriptions': oldEventName = 'subscriptions_view'; oldEventProps.section = 'helper'; diff --git a/plugins/woocommerce/changelog/51309-update-21591-in-app-category-selector b/plugins/woocommerce/changelog/51309-update-21591-in-app-category-selector new file mode 100644 index 00000000000..504e3d4d2d2 --- /dev/null +++ b/plugins/woocommerce/changelog/51309-update-21591-in-app-category-selector @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update In-App Marketplace category selector \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51313-update-wccom-21595-in-app-search-component b/plugins/woocommerce/changelog/51313-update-wccom-21595-in-app-search-component new file mode 100644 index 00000000000..42a06d9bf86 --- /dev/null +++ b/plugins/woocommerce/changelog/51313-update-wccom-21595-in-app-search-component @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Replace marketplace search component with SearchControl from @wordpress/components \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51342-fix-21596-search-loading-state b/plugins/woocommerce/changelog/51342-fix-21596-search-loading-state new file mode 100644 index 00000000000..a55cd0e6601 --- /dev/null +++ b/plugins/woocommerce/changelog/51342-fix-21596-search-loading-state @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix the loading state for the In-App Marketplace search \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51434-add-wccom-21568-in-app-load-more-button b/plugins/woocommerce/changelog/51434-add-wccom-21568-in-app-load-more-button new file mode 100644 index 00000000000..50f6c905990 --- /dev/null +++ b/plugins/woocommerce/changelog/51434-add-wccom-21568-in-app-load-more-button @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Added a Load More button to product lists on the Extensions page, to request additional search results from WooCommerce.com. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/refactor-wccom-21576-search-tab b/plugins/woocommerce/changelog/refactor-wccom-21576-search-tab new file mode 100644 index 00000000000..ba8046f3451 --- /dev/null +++ b/plugins/woocommerce/changelog/refactor-wccom-21576-search-tab @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Change the way search results are displayed in the in-app marketplace diff --git a/plugins/woocommerce/changelog/tweak-21597-in-app-search-results-count b/plugins/woocommerce/changelog/tweak-21597-in-app-search-results-count new file mode 100644 index 00000000000..b0edda219c3 --- /dev/null +++ b/plugins/woocommerce/changelog/tweak-21597-in-app-search-results-count @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add search result counts to the in-app marketplace header tabs (Extensions area) From 610af536d120f75495d2af664f94bc29d154f51a Mon Sep 17 00:00:00 2001 From: DAnn2012 Date: Wed, 18 Sep 2024 14:15:51 +0200 Subject: [PATCH 06/20] Fix typo (adding-a-custom-field-to-variable-products.md) (#50331) Update adding-a-custom-field-to-variable-products.md Co-authored-by: Seghir Nadir --- .../adding-a-custom-field-to-variable-products.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md b/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md index 3b7a53c9fe8..d5cbef24096 100644 --- a/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md +++ b/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md @@ -254,4 +254,4 @@ Displaying the variation in the front store works a bit differently for variable ## How to find hooks? -Everyone will have their own preferred way, but for me, the quickest way is to look in the WooCommere plugin code. The code for each data section can be found in `/woocommerce/includes/admin/meta-boxes/views`. To view how the inventory section is handled check the `html-product-data-inventory.php` file, and for variations take a look at `html-variation-admin.php`. +Everyone will have their own preferred way, but for me, the quickest way is to look in the WooCommerce plugin code. The code for each data section can be found in `/woocommerce/includes/admin/meta-boxes/views`. To view how the inventory section is handled check the `html-product-data-inventory.php` file, and for variations take a look at `html-variation-admin.php`. From 8042fcdae351b7dd5e8004c3ffad63cdbc7926e1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 19:32:28 +0700 Subject: [PATCH 07/20] Delete changelog files based on PR 51456 (#51496) Delete changelog files for 51456 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/pr-51456 | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/pr-51456 diff --git a/plugins/woocommerce/changelog/pr-51456 b/plugins/woocommerce/changelog/pr-51456 deleted file mode 100644 index 9d71edd5fbf..00000000000 --- a/plugins/woocommerce/changelog/pr-51456 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix invalid path error in product importer in Windows From 13f5eee40b522307370e7aa8d8bd1de5414d6751 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 19:33:24 +0700 Subject: [PATCH 08/20] Delete changelog files based on PR 51441 (#51497) Delete changelog files for 51441 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/revert-low-stock-notification | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/revert-low-stock-notification diff --git a/plugins/woocommerce/changelog/revert-low-stock-notification b/plugins/woocommerce/changelog/revert-low-stock-notification deleted file mode 100644 index 3ec370d906f..00000000000 --- a/plugins/woocommerce/changelog/revert-low-stock-notification +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Revert - changes related to low stock product notification From df37ccf8c596b42840af895ff9458418de353b8e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 19:34:07 +0700 Subject: [PATCH 09/20] Delete changelog files based on PR 51449 (#51477) Delete changelog files for 51449 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/51449-dev-harden-added-to-cart | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/51449-dev-harden-added-to-cart diff --git a/plugins/woocommerce/changelog/51449-dev-harden-added-to-cart b/plugins/woocommerce/changelog/51449-dev-harden-added-to-cart deleted file mode 100644 index 99351de4130..00000000000 --- a/plugins/woocommerce/changelog/51449-dev-harden-added-to-cart +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix bug where manually triggering `added_to_cart` event without a button element caused an Exception. \ No newline at end of file From 9d135ee2f51a654221ef2a2249d844d4062adf6b Mon Sep 17 00:00:00 2001 From: Jonathan Lane Date: Wed, 18 Sep 2024 09:43:12 -0700 Subject: [PATCH 10/20] Skip top flaky tests (#51510) * Skip top flaky tests * Add changefile(s) from automation for the following project(s): woocommerce-blocks --------- Co-authored-by: Jon Lane Co-authored-by: github-actions --- .../local-pickup/local-pickup.merchant.block_theme.spec.ts | 2 +- .../product-collection.block_theme.spec.ts | 6 +++++- .../register-product-collection-tester.block_theme.spec.ts | 4 ++-- .../51510-e2e-skip-skip-flaky-e2e-tests-until-fixed | 4 ++++ 4 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 plugins/woocommerce/changelog/51510-e2e-skip-skip-flaky-e2e-tests-until-fixed diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/local-pickup/local-pickup.merchant.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/local-pickup/local-pickup.merchant.block_theme.spec.ts index 6bdf8015ca2..d48c78315c9 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/local-pickup/local-pickup.merchant.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/local-pickup/local-pickup.merchant.block_theme.spec.ts @@ -184,7 +184,7 @@ test.describe( 'Merchant → Local Pickup Settings', () => { ).toBeVisible(); } ); - test( 'updating the title in WC Settings updates the local pickup text in the block and vice/versa', async ( { + test.skip( 'updating the title in WC Settings updates the local pickup text in the block and vice/versa', async ( { page, localPickupUtils, admin, diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.spec.ts index bb4f6bd8f75..bc984dbb627 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.spec.ts @@ -87,7 +87,11 @@ test.describe( 'Product Collection', () => { await admin.createNewPost(); } ); - test( 'does not render', async ( { page, editor, pageObject } ) => { + test.skip( 'does not render', async ( { + page, + editor, + pageObject, + } ) => { await pageObject.insertProductCollection(); await pageObject.chooseCollectionInPost( 'featured' ); await pageObject.addFilter( 'Price Range' ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/register-product-collection-tester.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/register-product-collection-tester.block_theme.spec.ts index 6fd09e4050c..d201ef01574 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/register-product-collection-tester.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/register-product-collection-tester.block_theme.spec.ts @@ -235,7 +235,7 @@ test.describe( 'Product Collection registration', () => { await expect( previewButtonLocator ).toBeHidden(); } ); - test( 'Should display properly in Product Catalog template', async ( { + test.skip( 'Should display properly in Product Catalog template', async ( { pageObject, editor, } ) => { @@ -357,7 +357,7 @@ test.describe( 'Product Collection registration', () => { } ); } ); - test( 'Product picker should be shown when selected product is deleted', async ( { + test.skip( 'Product picker should be shown when selected product is deleted', async ( { pageObject, admin, editor, diff --git a/plugins/woocommerce/changelog/51510-e2e-skip-skip-flaky-e2e-tests-until-fixed b/plugins/woocommerce/changelog/51510-e2e-skip-skip-flaky-e2e-tests-until-fixed new file mode 100644 index 00000000000..b34a0554f9f --- /dev/null +++ b/plugins/woocommerce/changelog/51510-e2e-skip-skip-flaky-e2e-tests-until-fixed @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak +Comment: This PR skips 4 e2e tests and makes no other changes + From 96719139beeea1e3d1d87d56143c74efa0313d92 Mon Sep 17 00:00:00 2001 From: Nathan Silveira Date: Wed, 18 Sep 2024 15:26:40 -0300 Subject: [PATCH 11/20] Add global_unique_id parameter to products REST API (#51264) * Add global_unique_id parameter to products REST API * Remove unnecessary if statement related to commas and trim request content --- .../changelog/update-rest-api-global-unique-id | 4 ++++ .../Version3/class-wc-rest-products-controller.php | 12 ++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 plugins/woocommerce/changelog/update-rest-api-global-unique-id diff --git a/plugins/woocommerce/changelog/update-rest-api-global-unique-id b/plugins/woocommerce/changelog/update-rest-api-global-unique-id new file mode 100644 index 00000000000..2a66b917137 --- /dev/null +++ b/plugins/woocommerce/changelog/update-rest-api-global-unique-id @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add global_unique_id parameter to products REST API diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php index f1c472a38cd..4e981a84ff0 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php @@ -259,6 +259,18 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller { } } + if ( ! empty( $request['global_unique_id'] ) ) { + $global_unique_ids = array_map( 'trim', explode( ',', $request['global_unique_id'] ) ); + $args['meta_query'] = $this->add_meta_query( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + $args, + array( + 'key' => '_global_unique_id', + 'value' => $global_unique_ids, + 'compare' => 'IN', + ) + ); + } + // Filter by tax class. if ( ! empty( $request['tax_class'] ) ) { $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. From 9b8256cc3e2b60e9c81b18bfc0e1d859515d929c Mon Sep 17 00:00:00 2001 From: Alex Florisca Date: Wed, 18 Sep 2024 20:49:27 +0100 Subject: [PATCH 12/20] [Feature] Express Checkout Improvements (#50791) * Add new buttonAttributes API to style express checkout buttons coherently (#47899) * Expose buttonAttributes to the express payment methods * Add size and label attributes to the express checkout area * Remove defaultHeight * default button Label * Remove the button label attribute * Remove px from height * Change large button height to 55px * Load express checkout block with attributes * Add toggle and borderRadius controls and remove getting border radius from the theme * Remove extra border radius text * Only pass buttonAttributes if toggled on * Move express payment block attribute logic into a Provider * Tidy up editor grid and parse attributes into context on frontend * Add px to border-radius input * Express payment methods not selectable * Add a test * lint fixes * default button height is 48 not 4 * Add changefile(s) from automation for the following project(s): woocommerce-blocks * Update docs * Add tests for express payment methods * Center images within the express payment area in the editor * Apply the buttonAttributes to the li container in the editor regardless of showButtonStyles * Fix style issue * fix linting * fix lint again * Update manifest * Update docs manifest * Resize images in editor * lint fix --------- Co-authored-by: github-actions * Add changefile(s) from automation for the following project(s): woocommerce-blocks, woocommerce * Synchronise the express payment settings between the Cart & Checkout blocks (#50688) * Add express payment methods to sidebar * Only add extra props for express payment methods * Update docs * Make title, description and gatewayId types optional * Update docs * Fix types again and editor side * Add changefile(s) from automation for the following project(s): woocommerce-blocks * handle situation when no methods are active * Update manifest * Add express payment methods to inspector controls for express checkout block (#50983) * Remove forced styles on the editor * Remove the darkMode setting from the buttonAttributes API (#51109) * Remove darkMode from the buttonAttributes API * Add changefile(s) from automation for the following project(s): woocommerce-blocks, woocommerce --------- Co-authored-by: github-actions * Accept supports declarations for express payment style controls + merchant ux improvements in the editor (#51296) * Fix images in editor displaying weird * Fix long express payment names breaking layout * Default to uniform styles off * Use heightControl for border radius and fix height for cart buttons * Move formatting title and description to the config validation * Add changefile(s) from automation for the following project(s): woocommerce-blocks, woocommerce * Fix linting * Fix failing test * Add back the 48px height for images in editor * Fix linting again * Update docs * Update docs manifest * Update docs to fix linting * Add comment to test to better explain why we are expecting a console warning * make strings translatable * Sync cart & checkout directly without option * Remove current styles * Change the beta label * Replace < and > with symbol references in docs * Update docs manifest * Increase padding of beta label * fix linter issues * change to using looger helper * fix CSS --------- Co-authored-by: github-actions Co-authored-by: Nadir Seghir --- .../payment-method-integration.md | 78 +++++-- docs/docs-manifest.json | 8 +- .../express-payment-method-config.ts | 18 ++ .../express-payment-settings.tsx | 202 ++++++++++++++++++ .../block-settings/index.tsx | 2 + ...methods.js => express-payment-methods.tsx} | 13 ++ ...ayment.js => checkout-express-payment.tsx} | 0 .../express-payment-context.ts | 21 ++ .../payment-methods/express-payment/index.js | 2 +- .../express-payment/style.scss | 3 +- .../test/__mocks__/editor-context.ts | 3 + .../test/__mocks__/express-payment-props.ts | 187 ++++++++++++++++ .../test/express-payment-methods.tsx | 139 ++++++++++++ .../js/blocks/cart-checkout-shared/types.ts | 23 ++ .../cart-express-payment-block/block.json | 12 ++ .../cart-express-payment-block/edit.tsx | 22 +- .../cart-express-payment-block/editor.scss | 35 +++ .../cart-express-payment-block/frontend.tsx | 28 ++- .../checkout-express-payment-block/block.json | 12 ++ .../checkout-express-payment-block/block.tsx | 1 - .../checkout-express-payment-block/edit.tsx | 26 ++- .../editor.scss | 35 +++ .../frontend.tsx | 32 +++ .../checkout-express-payment-block/types.ts | 10 + .../inner-blocks/register-components.ts | 2 +- .../payment/test/check-payment-methods.tsx | 3 + .../assets/js/data/payment/test/selectors.js | 3 + .../payment/utils/check-payment-methods.ts | 29 ++- .../assets/js/types/type-defs/payments.ts | 41 +++- ...899-try-poc-express-checkout-button-styles | 4 + .../changelog/50688-add-sync-cart-checkout | 4 + ...50791-feature-express-payment-improvements | 4 + ...983-add-express-payment-methods-to-sidebar | 4 + .../changelog/51109-remove-dark-mode | 4 + ...1296-try-supports-express-payment-controls | 4 + .../BlockTypes/CartExpressPaymentBlock.php | 14 ++ .../CheckoutExpressPaymentBlock.php | 127 +++++++++++ .../src/Blocks/Utils/BlockTemplateUtils.php | 4 +- .../src/Blocks/Utils/CartCheckoutUtils.php | 47 +++- 39 files changed, 1155 insertions(+), 51 deletions(-) create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/block-settings/express-payment-settings.tsx rename plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/{express-payment-methods.js => express-payment-methods.tsx} (92%) rename plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/{checkout-express-payment.js => checkout-express-payment.tsx} (100%) create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/express-payment-context.ts create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/__mocks__/editor-context.ts create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/__mocks__/express-payment-props.ts create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/express-payment-methods.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/types.ts create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/frontend.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/types.ts create mode 100644 plugins/woocommerce/changelog/47899-try-poc-express-checkout-button-styles create mode 100644 plugins/woocommerce/changelog/50688-add-sync-cart-checkout create mode 100644 plugins/woocommerce/changelog/50791-feature-express-payment-improvements create mode 100644 plugins/woocommerce/changelog/50983-add-express-payment-methods-to-sidebar create mode 100644 plugins/woocommerce/changelog/51109-remove-dark-mode create mode 100644 plugins/woocommerce/changelog/51296-try-supports-express-payment-controls diff --git a/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md b/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md index d3457214654..664e64370ab 100644 --- a/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md +++ b/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md @@ -43,12 +43,16 @@ The options you feed the configuration instance should be an object in this shap ```js const options = { name: 'my_payment_method', - content: <div>A React node</div>, - edit: <div>A React node</div>, + title: 'My Mayment Method', + description: 'A setence or two about your payment method', + gatewayId: 'gateway-id', + content: <ReactNode />, + edit: <ReactNode />, canMakePayment: () => true, paymentMethodId: 'new_payment_method', supports: { features: [], + style: [], }, }; ``` @@ -59,6 +63,18 @@ Here's some more details on the configuration options: This should be a unique string (wise to try to pick something unique for your gateway that wouldn't be used by another implementation) that is used as the identifier for the gateway client side. If `paymentMethodId` is not provided, `name` is used for `paymentMethodId` as well. +#### `title` (optional) + +This should be a human readable string with the name of your payment method. It should be sentence capitalised. It is displayed to the merchant in the editor when viewing the Checkout block to indicate which express payment methods are active. If it is not provided, the `name` will be used as the title. + +#### `description` (optional) + +This is one or two sentences maximum describing your payment gateway. It should be sentence capitalised. It is displayed to the merchant in the editor when viewing the Checkout block to indicate which express payment methods are active. + +#### `gatewayId` (optional) + +This is the ID of the Payment Gateway that your plugin registers server side, and which registers the express payment method. It is used to link your express payment method on the clinet, to a payment gateway defined on the server. It is used to direct the merchant to the right settings page within the editor. If this is not provided, the merchant will be redirected to the general Woo payment settings page. + #### `content` (required) This should be a React node that will output in the express payment method area when the block is rendered in the frontend. It will be cloned in the rendering process. When cloned, this React node will receive props passed in from the checkout payment method interface that will allow your component to interact with checkout data (more on [these props later](#props-fed-to-payment-method-nodes)). @@ -97,7 +113,11 @@ This is the only optional configuration object. The value of this property is wh This is an array of payment features supported by the gateway. It is used to crosscheck if the payment method can be used for the content of the cart. By default payment methods should support at least `products` feature. If no value is provided then this assumes that `['products']` are supported. ---- +#### `supports:style` + +This is an array of style variations supported by the express payment method. These are styles that are applied across all the active express payment buttons and can be controlled from the express payment block in the editor. Supported values for these are one of `['height', 'borderRadius']`. + +![Express Checkout Uniform Styles](https://github.com/user-attachments/assets/f0f99f3f-dca7-42b0-8685-3b098a825020) ### Payment Methods - `registerPaymentMethod( options )` @@ -139,23 +159,24 @@ The options you feed the configuration instance are the same as those for expres A big part of the payment method integration is the interface that is exposed for payment methods to use via props when the node provided is cloned and rendered on block mount. While all the props are listed below, you can find more details about what the props reference, their types etc via the [typedefs described in this file](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/assets/js/types/type-defs/payment-method-interface.ts). -| Property | Type | Description | Values | -| ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `activePaymentMethod` | String | The slug of the current active payment method in the checkout. | - | -| `billing` | Object | Contains everything related to billing. | `billingAddress`, `cartTotal`, `currency`, `cartTotalItems`, `displayPricesIncludingTax`, `appliedCoupons`, `customerId` | -| `cartData` | Object | Data exposed from the cart including items, fees, and any registered extension data. Note that this data should be treated as immutable (should not be modified/mutated) or it will result in errors in your application. | `cartItems`, `cartFees`, `extensions` | -| `checkoutStatus` | Object | The current checkout status exposed as various boolean state. | `isCalculating`, `isComplete`, `isIdle`, `isProcessing` | +| Property | Type | Description | Values | +| ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `activePaymentMethod` | String | The slug of the current active payment method in the checkout. | - | +| `billing` | Object | Contains everything related to billing. | `billingAddress`, `cartTotal`, `currency`, `cartTotalItems`, `displayPricesIncludingTax`, `appliedCoupons`, `customerId` | +| `cartData` | Object | Data exposed from the cart including items, fees, and any registered extension data. Note that this data should be treated as immutable (should not be modified/mutated) or it will result in errors in your application. | `cartItems`, `cartFees`, `extensions` | +| `checkoutStatus` | Object | The current checkout status exposed as various boolean state. | `isCalculating`, `isComplete`, `isIdle`, `isProcessing` | | `components` | Object | It exposes React components that can be implemented by your payment method for various common interface elements used by payment methods. |
    • `ValidationInputError`: a container for holding validation errors which typically you'll include after any inputs.
    • [`PaymentMethodLabel`](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/e089ae17043fa525e8397d605f0f470959f2ae95/assets/js/payment-method-extensions/payment-methods/paypal/index.js#L37-L40): use this component for the payment method label, including an optional icon.
    • `PaymentMethodIcons`: a React component used for displaying payment method icons.
    • - `LoadingMask`: a wrapper component that handles displaying a loading state when the isLoading prop is true. Exposes the [LoadingMask component](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/c9074a4941919987dbad16a80f358b960336a09d/assets/js/base/components/loading-mask/index.js)
    | | `emitResponse` | Object | Contains some constants that can be helpful when using the event emitter. Read the _[Emitting Events](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/e267cd96a4329a4eeef816b2ef627e113ebb72a5/docs/extensibility/checkout-flow-and-events.md#emitting-events)_ section for more details. |
    • `noticeContexts`: This is an object containing properties referencing areas where notices can be targeted in the checkout. The object has the following properties:
      • `PAYMENTS`: This is a reference to the notice area in the payment methods step.
      • `EXPRESS_PAYMENTS`: This is a reference to the notice area in the express payment methods step.
    • `responseTypes`: This is an object containing properties referencing the various response types that can be returned by observers for some event emitters. It makes it easier for autocompleting the types and avoiding typos due to human error. The types are `SUCCESS`, `FAIL`, `ERROR`. The values for these types also correspond to the [payment status types](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/34e17c3622637dbe8b02fac47b5c9b9ebf9e3596/src/Payments/PaymentResult.php#L21) from the [checkout endpoint response from the server](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/34e17c3622637dbe8b02fac47b5c9b9ebf9e3596/src/RestApi/StoreApi/Schemas/CheckoutSchema.php#L103-L113).
    | -| `eventRegistration` | object | Contains all the checkout event emitter registration functions. These are functions the payment method can register observers on to interact with various points in the checkout flow (see [this doc](./checkout-flow-and-events.md) for more info). | `onCheckoutValidation`, `onCheckoutSuccess`, `onCheckoutFail`, `onPaymentSetup`, `onShippingRateSuccess`, `onShippingRateFail`, `onShippingRateSelectSuccess`, `onShippingRateSelectFail` | -| `onClick` | Function | **Provided to express payment methods** that should be triggered when the payment method button is clicked (which will signal to checkout the payment method has taken over payment processing) | - | -| `onClose` | Function | **Provided to express payment methods** that should be triggered when the express payment method modal closes and control is returned to checkout. | - | -| `onSubmit` | Function | Submits the checkout and begins processing | - | -| `paymentStatus` | Object | Various payment status helpers. Note, your payment method does not have to handle setting this status client side. Checkout will handle this via the responses your payment method gives from observers registered to [checkout event emitters](./checkout-flow-and-events.md). | `isPristine`, `isStarted`, `isProcessing`, `isFinished`, `hasError`, `hasFailed`, `isSuccessful` (see below for explanation) | -| `setExpressPaymentError` | Function | Receives a string and allows express payment methods to set an error notice for the express payment area on demand. This can be necessary because some express payment method processing might happen outside of checkout events. | - | -| `shippingData` | Object | Contains all shipping related data (outside of the shipping status). | `shippingRates`, `shippingRatesLoading`, `selectedRates`, `setSelectedRates`, `isSelectingRate`, `shippingAddress`, `setShippingAddress`, and `needsShipping` | +| `eventRegistration` | object | Contains all the checkout event emitter registration functions. These are functions the payment method can register observers on to interact with various points in the checkout flow (see [this doc](./checkout-flow-and-events.md) for more info). | `onCheckoutValidation`, `onCheckoutSuccess`, `onCheckoutFail`, `onPaymentSetup`, `onShippingRateSuccess`, `onShippingRateFail`, `onShippingRateSelectSuccess`, `onShippingRateSelectFail` | +| `onClick` | Function | **Provided to express payment methods** that should be triggered when the payment method button is clicked (which will signal to checkout the payment method has taken over payment processing) | - | +| `onClose` | Function | **Provided to express payment methods** that should be triggered when the express payment method modal closes and control is returned to checkout. | - | +| `onSubmit` | Function | Submits the checkout and begins processing | - | +| `buttonAttributes` | Object | Styles set by the merchant that should be respected by all express payment buttons | `height, borderRadius` | +| `paymentStatus` | Object | Various payment status helpers. Note, your payment method does not have to handle setting this status client side. Checkout will handle this via the responses your payment method gives from observers registered to [checkout event emitters](./checkout-flow-and-events.md). | `isPristine`, `isStarted`, `isProcessing`, `isFinished`, `hasError`, `hasFailed`, `isSuccessful`(see below for explanation) | +| `setExpressPaymentError` | Function | Receives a string and allows express payment methods to set an error notice for the express payment area on demand. This can be necessary because some express payment method processing might happen outside of checkout events. | - | +| `shippingData` | Object | Contains all shipping related data (outside of the shipping status). | `shippingRates`, `shippingRatesLoading`, `selectedRates`, `setSelectedRates`, `isSelectingRate`, `shippingAddress`, `setShippingAddress`, and `needsShipping` | | `shippingStatus` | Object | Various shipping status helpers. |
    • `shippingErrorStatus`: an object with various error statuses that might exist for shipping
    • `shippingErrorTypes`: an object containing all the possible types for shipping error status
    | -| `shouldSavePayment` | Boolean | Indicates whether or not the shopper has selected to save their payment method details (for payment methods that support saved payments). True if selected, false otherwise. Defaults to false. | - | +| `shouldSavePayment` | Boolean | Indicates whether or not the shopper has selected to save their payment method details (for payment methods that support saved payments). True if selected, false otherwise. Defaults to false. | - | - `isPristine`: This is true when the current payment status is `PRISTINE`. - `isStarted`: This is true when the current payment status is `EXPRESS_STARTED`. @@ -167,6 +188,29 @@ A big part of the payment method integration is the interface that is exposed fo Any registered `savedTokenComponent` node will also receive a `token` prop which includes the id for the selected saved token in case your payment method needs to use it for some internal logic. However, keep in mind, this is just the id representing this token in the database (and the value of the radio input the shopper checked), not the actual customer payment token (since processing using that usually happens on the server for security). +### Button Attributes for Express Payment Methods + +This API provides a way to synchronise the look and feel of the express payment buttons for a coherent shopper experience. Express Payment Methods must prefer the values provided in the `buttonAttributes`, and use it's own configuration settings as backup when the buttons are rendered somewhere other than the Cart or Checkout block. + +For example, in your button component, you would do something like this: + +```js +// Get your extension specific settings and set defaults if not available +let { + borderRadius = '4', + height = '48', +} = getButtonSettingsFromConfig(); + +// In a cart & checkout block context, we receive `buttonAttributes` as a prop which overwrite the extension specific settings +if ( typeof buttonAttributes !== 'undefined' ) { + height = buttonAttributes.height; + borderRadius = buttonAttributes.borderRadius; +} +... + +return <button style={height: `${height}px`, borderRadius: `${borderRadius}px`} /> +``` + ## Server Side Integration ### Processing Payment diff --git a/docs/docs-manifest.json b/docs/docs-manifest.json index d18a4367869..0b71e764ab1 100644 --- a/docs/docs-manifest.json +++ b/docs/docs-manifest.json @@ -83,7 +83,7 @@ "menu_title": "Add Custom Fields to Products", "tags": "how-to", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md", - "hash": "fe8cf43940f5166bf69f102aa4643cbe32415b1167d6b6d8968d434a4d113879", + "hash": "df61c93febc234fe0dbb4826a20ae120b153ab6f6c92d8778177fcac8d6696fe", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md", "id": "64b686dcd5fdd4842be2fc570108231d5a8bfc1b" } @@ -223,7 +223,7 @@ "menu_title": "Payment Method Integration", "tags": "reference", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md", - "hash": "138ffbf27e79ec8b35d2c46e87e3663c203d91fc9ba3f76c43f3cbe76258e5bf", + "hash": "015aae25bb331364c224fe8eb2b7675e4cbed0a9e6bee0dde5f5311388161b0a", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md", "id": "c9a763b6976ecf03aeb961577c17c31f1ac7c420", "links": { @@ -1229,7 +1229,7 @@ "menu_title": "Core critical flows", "tags": "reference", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/core-critical-flows.md", - "hash": "34109195216ebcb5b23e741391b9f355ba861777a5533d4ef1e341472cb5209e", + "hash": "c7122979df14f46646b3f1472ba071bc560b99e6462c5790a9aeaa3b4238ce15", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/core-critical-flows.md", "id": "e561b46694dba223c38b87613ce4907e4e14333a" }, @@ -1804,5 +1804,5 @@ "categories": [] } ], - "hash": "47dc2a7e213e1e9d83e93a85dafdf8c7b539cc5474b4166eb6398b734d150ff3" + "hash": "a88d9ea54465c8bbd820042a92df79cbd48943e785b418fcaa04d0c0e66116c0" } \ No newline at end of file diff --git a/plugins/woocommerce-blocks/assets/js/blocks-registry/payment-methods/express-payment-method-config.ts b/plugins/woocommerce-blocks/assets/js/blocks-registry/payment-methods/express-payment-method-config.ts index 4e039c9d6ac..38ab4568d2b 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks-registry/payment-methods/express-payment-method-config.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks-registry/payment-methods/express-payment-method-config.ts @@ -19,6 +19,9 @@ export default class ExpressPaymentMethodConfig implements ExpressPaymentMethodConfigInstance { public name: string; + public title: string; + public description: string; + public gatewayId: string; public content: ReactNode; public edit: ReactNode; public paymentMethodId?: string; @@ -27,13 +30,28 @@ export default class ExpressPaymentMethodConfig constructor( config: ExpressPaymentMethodConfiguration ) { // validate config + + const readableName = + typeof config.name === 'string' + ? config.name.replace( /[_-]/g, ' ' ) + : config.name; + const trimedDescription = + typeof config?.description === 'string' && + config.description.length > 130 + ? config.description.slice( 0, 130 ) + '...' + : config.description; + ExpressPaymentMethodConfig.assertValidConfig( config ); this.name = config.name; + this.title = config.title || readableName; + this.description = trimedDescription || ''; + this.gatewayId = config.gatewayId || ''; this.content = config.content; this.edit = config.edit; this.paymentMethodId = config.paymentMethodId || this.name; this.supports = { features: config?.supports?.features || [ 'products' ], + style: config?.supports?.style || [], }; this.canMakePaymentFromConfig = config.canMakePayment; } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/block-settings/express-payment-settings.tsx b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/block-settings/express-payment-settings.tsx new file mode 100644 index 00000000000..cc352b10f4e --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/block-settings/express-payment-settings.tsx @@ -0,0 +1,202 @@ +/** + * External dependencies + */ +import { InspectorControls, HeightControl } from '@wordpress/block-editor'; +import { + PanelBody, + ToggleControl, + RadioControl, + Notice, +} from '@wordpress/components'; +import ExternalLinkCard from '@woocommerce/editor-components/external-link-card'; +import { __ } from '@wordpress/i18n'; +import type { BlockAttributes } from '@wordpress/blocks'; +import { select } from '@wordpress/data'; +import { PAYMENT_STORE_KEY } from '@woocommerce/block-data'; +import { ADMIN_URL } from '@woocommerce/settings'; + +const allStyleControls = [ 'height', 'borderRadius' ]; + +const atLeastOnePaymentMethodSupportsOneOf = ( styleControl: string[] ) => { + const availableExpressMethods = + select( PAYMENT_STORE_KEY ).getAvailableExpressPaymentMethods(); + + return Object.values( availableExpressMethods ).reduce( + ( acc, currentValue ) => { + return ( + acc || + currentValue?.supportsStyle.some( ( el ) => + styleControl.includes( el ) + ) + ); + }, + false + ); +}; + +const ExpressPaymentButtonStyleControls = ( { + attributes, + setAttributes, +}: { + attributes: BlockAttributes; + setAttributes: ( attrs: BlockAttributes ) => void; +} ) => { + const { buttonHeight, buttonBorderRadius } = attributes; + + return ( + <> + { atLeastOnePaymentMethodSupportsOneOf( [ 'height' ] ) && ( + + setAttributes( { buttonHeight: newValue } ) + } + /> + ) } + { atLeastOnePaymentMethodSupportsOneOf( [ 'borderRadius' ] ) && ( +
    + { + const valueOnly = newValue.replace( 'px', '' ); + setAttributes( { + buttonBorderRadius: valueOnly, + } ); + } } + /> +
    + ) } + + ); +}; + +const ExpressPaymentToggle = ( { + attributes, + setAttributes, +}: { + attributes: BlockAttributes; + setAttributes: ( attrs: BlockAttributes ) => void; +} ) => { + if ( attributes.showButtonStyles ) { + return ( + + ); + } + return null; +}; + +const ExpressPaymentMethods = () => { + const availableExpressMethods = + select( PAYMENT_STORE_KEY ).getAvailableExpressPaymentMethods(); + + if ( Object.entries( availableExpressMethods ).length < 1 ) { + return ( +

    + { __( + 'You currently have no express payment integrations active.', + 'woocommerce' + ) } +

    + ); + } + + return ( + <> +

    + { __( + 'You currently have the following express payment integrations active.', + 'woocommerce' + ) } +

    + { Object.values( availableExpressMethods ).map( ( values ) => { + return ( + + ); + } ) } + + ); +}; + +const toggleLabel = ( + <> + { __( 'Apply uniform styles', 'woocommerce' ) }{ ' ' } + Beta + +); + +export const ExpressPaymentControls = ( { + attributes, + setAttributes, +}: { + attributes: BlockAttributes; + setAttributes: ( attrs: BlockAttributes ) => void; +} ) => { + return ( + + { atLeastOnePaymentMethodSupportsOneOf( allStyleControls ) && ( + + + setAttributes( { + showButtonStyles: ! attributes.showButtonStyles, + } ) + } + help={ __( + 'Sets a consistent style for express payment buttons.', + 'woocommerce' + ) } + /> + + { __( 'Note', 'woocommerce' ) }:{ ' ' } + { __( + 'Some payment methods might not yet support all style controls', + 'woocommerce' + ) } + + + + ) } + + + + + ); +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/block-settings/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/block-settings/index.tsx index c0794a295b0..3bc73292d8f 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/block-settings/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/block-settings/index.tsx @@ -34,3 +34,5 @@ export const BlockSettings = ( { ); }; + +export { ExpressPaymentControls } from './express-payment-settings'; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.js b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.tsx similarity index 92% rename from plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.js rename to plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.tsx index af627dacac1..466c6c863d4 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.js +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.tsx @@ -21,10 +21,22 @@ import { useDispatch, useSelect } from '@wordpress/data'; */ import PaymentMethodErrorBoundary from './payment-method-error-boundary'; import { STORE_KEY as PAYMENT_STORE_KEY } from '../../../data/payment/constants'; +import { useExpressPaymentContext } from '../../cart-checkout-shared/payment-methods/express-payment/express-payment-context'; const ExpressPaymentMethods = () => { const { isEditor } = useEditorContext(); + const { showButtonStyles, buttonHeight, buttonBorderRadius } = + useExpressPaymentContext(); + + // API for passing styles to express payment buttons + const buttonAttributes = showButtonStyles + ? { + height: buttonHeight, + borderRadius: buttonBorderRadius, + } + : undefined; + const { activePaymentMethod, paymentMethodData } = useSelect( ( select ) => { const store = select( PAYMENT_STORE_KEY ); @@ -150,6 +162,7 @@ const ExpressPaymentMethods = () => { onError: onExpressPaymentError, setExpressPaymentError: deprecatedSetExpressPaymentError, + buttonAttributes, } ) } ) : null; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.js b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.tsx similarity index 100% rename from plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.js rename to plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.tsx diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/express-payment-context.ts b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/express-payment-context.ts new file mode 100644 index 00000000000..4526987c1a8 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/express-payment-context.ts @@ -0,0 +1,21 @@ +/** + * External dependencies + */ +import { useContext, createContext } from '@wordpress/element'; + +type ExpressPaymentContextProps = { + showButtonStyles: boolean; + buttonHeight: string; + buttonBorderRadius: string; +}; + +export const ExpressPaymentContext: React.Context< ExpressPaymentContextProps > = + createContext< ExpressPaymentContextProps >( { + showButtonStyles: false, + buttonHeight: '48', + buttonBorderRadius: '4', + } ); + +export const useExpressPaymentContext = () => { + return useContext( ExpressPaymentContext ); +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/index.js b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/index.js index 62836b04504..2bca0b47405 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/index.js +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/index.js @@ -1,2 +1,2 @@ export { default as CartExpressPayment } from './cart-express-payment.js'; -export { default as CheckoutExpressPayment } from './checkout-express-payment.js'; +export { default as CheckoutExpressPayment } from './checkout-express-payment'; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss index 100962f2501..56720d7d55b 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss @@ -14,6 +14,7 @@ $border-width: 1px; > li { margin: 0; width: 100%; + overflow: hidden; > img { width: 100%; @@ -95,7 +96,7 @@ $border-width: 1px; .wc-block-components-express-payment--cart { .wc-block-components-express-payment__event-buttons { > li { - padding-bottom: $gap; + padding-bottom: $gap-small; text-align: center; width: 100%; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/__mocks__/editor-context.ts b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/__mocks__/editor-context.ts new file mode 100644 index 00000000000..441521f5750 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/__mocks__/editor-context.ts @@ -0,0 +1,3 @@ +// This needs to be defined in a separate file because we are mocking an import. +// The only way to do this is to define the mock and import it BEFORE the module being mocked. +export default jest.fn( () => ( { isEditor: false } ) ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/__mocks__/express-payment-props.ts b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/__mocks__/express-payment-props.ts new file mode 100644 index 00000000000..cc3873ad1e9 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/__mocks__/express-payment-props.ts @@ -0,0 +1,187 @@ +// This is the shape of the API exposed to the express payment methods via props +// Note that this is a public API! +export const getExpectedExpressPaymentProps = ( name: string ) => ( { + activePaymentMethod: undefined, + billing: { + appliedCoupons: [], + billingAddress: { + address_1: '', + address_2: '', + city: '', + company: '', + country: '', + first_name: '', + last_name: '', + phone: '', + postcode: '', + state: '', + }, + billingData: { + address_1: '', + address_2: '', + city: '', + company: '', + country: '', + first_name: '', + last_name: '', + phone: '', + postcode: '', + state: '', + }, + cartTotal: { + label: 'Total', + value: 0, + }, + cartTotalItems: [ + { + key: 'total_items', + label: 'Subtotal:', + value: 0, + valueWithTax: 0, + }, + { + key: 'total_fees', + label: 'Fees:', + value: 0, + valueWithTax: 0, + }, + { + key: 'total_discount', + label: 'Discount:', + value: 0, + valueWithTax: 0, + }, + { + key: 'total_tax', + label: 'Taxes:', + value: 0, + valueWithTax: 0, + }, + { + key: 'total_shipping', + label: 'Shipping:', + value: 0, + valueWithTax: 0, + }, + ], + currency: { + code: 'USD', + decimalSeparator: '.', + minorUnit: 2, + prefix: '$', + suffix: '', + symbol: '$', + thousandSeparator: ',', + }, + customerId: 1, + displayPricesIncludingTax: false, + }, + buttonAttributes: { + borderRadius: '4', + height: '48', + }, + cartData: { + cartFees: [], + cartItems: [], + extensions: {}, + }, + checkoutStatus: { + isCalculating: false, + isComplete: false, + isIdle: true, + isProcessing: false, + }, + components: { + LoadingMask: expect.any( Function ), + PaymentMethodIcons: expect.any( Function ), + PaymentMethodLabel: expect.any( Function ), + ValidationInputError: expect.any( Function ), + }, + emitResponse: { + noticeContexts: { + BILLING_ADDRESS: 'wc/checkout/billing-address', + CART: 'wc/cart', + CHECKOUT: 'wc/checkout', + CHECKOUT_ACTIONS: 'wc/checkout/checkout-actions', + CONTACT_INFORMATION: 'wc/checkout/contact-information', + EXPRESS_PAYMENTS: 'wc/checkout/express-payments', + ORDER_INFORMATION: 'wc/checkout/additional-information', + PAYMENTS: 'wc/checkout/payments', + SHIPPING_ADDRESS: 'wc/checkout/shipping-address', + SHIPPING_METHODS: 'wc/checkout/shipping-methods', + }, + responseTypes: { + ERROR: 'error', + FAIL: 'failure', + SUCCESS: 'success', + }, + }, + eventRegistration: { + onCheckoutAfterProcessingWithError: expect.any( Function ), + onCheckoutAfterProcessingWithSuccess: expect.any( Function ), + onCheckoutBeforeProcessing: expect.any( Function ), + onCheckoutFail: expect.any( Function ), + onCheckoutSuccess: expect.any( Function ), + onCheckoutValidation: expect.any( Function ), + onCheckoutValidationBeforeProcessing: expect.any( Function ), + onPaymentProcessing: expect.any( Function ), + onPaymentSetup: expect.any( Function ), + onShippingRateFail: expect.any( Function ), + onShippingRateSelectFail: expect.any( Function ), + onShippingRateSelectSuccess: expect.any( Function ), + onShippingRateSuccess: expect.any( Function ), + }, + name, + onClick: expect.any( Function ), + onClose: expect.any( Function ), + onError: expect.any( Function ), + onSubmit: expect.any( Function ), + paymentStatus: { + hasError: false, + hasFailed: false, + isDoingExpressPayment: false, + isFinished: false, + isIdle: true, + isPristine: true, + isProcessing: false, + isReady: false, + isStarted: false, + isSuccessful: false, + }, + setExpressPaymentError: expect.any( Function ), + shippingData: { + isSelectingRate: false, + needsShipping: true, + selectedRates: {}, + setSelectedRates: expect.any( Function ), + setShippingAddress: expect.any( Function ), + shippingAddress: { + address_1: '', + address_2: '', + city: '', + company: '', + country: '', + first_name: '', + last_name: '', + phone: '', + postcode: '', + state: '', + }, + shippingRates: [], + shippingRatesLoading: false, + }, + shippingStatus: { + shippingErrorStatus: { + hasError: false, + hasInvalidAddress: false, + isPristine: true, + isValid: false, + }, + shippingErrorTypes: { + INVALID_ADDRESS: 'invalid_address', + NONE: 'none', + UNKNOWN: 'unknown_error', + }, + }, + shouldSavePayment: false, +} ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/express-payment-methods.tsx b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/express-payment-methods.tsx new file mode 100644 index 00000000000..94b2e8e43de --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/express-payment-methods.tsx @@ -0,0 +1,139 @@ +/** + * External dependencies + */ +import { render, screen } from '@testing-library/react'; +import { PAYMENT_STORE_KEY } from '@woocommerce/block-data'; +import { + registerExpressPaymentMethod, + __experimentalDeRegisterExpressPaymentMethod, +} from '@woocommerce/blocks-registry'; +import { dispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import mockEditorContext from './__mocks__/editor-context'; +import { getExpectedExpressPaymentProps } from './__mocks__/express-payment-props'; +import ExpressPaymentMethods from '../express-payment-methods'; +jest.mock( '@woocommerce/base-context', () => ( { + useEditorContext: mockEditorContext, +} ) ); + +// Button styles are disabled by default. We need to mock the express payment context +// to enable them. +jest.mock( '../express-payment/express-payment-context', () => { + return { + useExpressPaymentContext: jest.fn().mockReturnValue( { + showButtonStyles: true, + buttonHeight: '48', + buttonBorderRadius: '4', + } ), + }; +} ); + +const mockExpressPaymentMethodNames = [ 'paypal', 'google pay', 'apple pay' ]; + +const MockExpressButton = jest.fn( ( { name } ) => ( +
    { `${ name } button` }
    +) ); + +const MockEditorExpressButton = jest.fn( ( { name } ) => ( +
    { `${ name } preview` }
    +) ); + +const registerMockExpressPaymentMethods = () => { + mockExpressPaymentMethodNames.forEach( ( name ) => { + registerExpressPaymentMethod( { + name, + title: `${ name } payment method`, + description: `A test ${ name } payment method`, + gatewayId: 'test-express-payment-method', + paymentMethodId: name, + content: , + edit: , + canMakePayment: () => true, + supports: { + features: [ 'products' ], + }, + } ); + } ); + dispatch( PAYMENT_STORE_KEY ).__internalUpdateAvailablePaymentMethods(); +}; + +const deregisterMockExpressPaymentMethods = () => { + mockExpressPaymentMethodNames.forEach( ( name ) => { + __experimentalDeRegisterExpressPaymentMethod( name ); + } ); +}; + +describe( 'Express payment methods', () => { + afterAll( () => { + jest.restoreAllMocks(); + } ); + describe( 'No payment methods available', () => { + it( 'should display no registered payment methods', () => { + render( ); + + const noPaymentMethods = screen.queryAllByText( + /No registered Payment Methods/ + ); + expect( noPaymentMethods.length ).toEqual( 1 ); + } ); + } ); + + describe( 'Payment methods available', () => { + beforeAll( () => { + registerMockExpressPaymentMethods(); + } ); + afterAll( () => { + deregisterMockExpressPaymentMethods(); + } ); + describe( 'In a frontend context', () => { + it( 'should display the element provided by paymentMethods.content', () => { + render( ); + mockExpressPaymentMethodNames.forEach( ( name ) => { + const btn = screen.getByText( `${ name } button` ); + expect( btn ).toBeVisible(); + } ); + } ); + it( 'should pass the correct properties to the rendered element', () => { + render( ); + mockExpressPaymentMethodNames.forEach( ( name ) => { + expect( MockExpressButton ).toHaveBeenCalledWith( + getExpectedExpressPaymentProps( name ), + {} + ); + } ); + // This is a bit out of place, but the console warning is triggered when the + // usePaymentMethodInterface hook is called so we need to expect it here otherwise + // the test fails on unexpected console warnings. + expect( console ).toHaveWarnedWith( + 'isPristine is deprecated since version 9.6.0. Please use isIdle instead. See: https://github.com/woocommerce/woocommerce-blocks/pull/8110' + ); + } ); + } ); + describe( 'In an editor context', () => { + beforeEach( () => { + mockEditorContext.mockImplementation( () => ( { + isEditor: true, + } ) ); + } ); + it( 'should display the element provided by paymentMethods.edit', () => { + render( ); + mockExpressPaymentMethodNames.forEach( ( name ) => { + const btn = screen.getByText( `${ name } preview` ); + expect( btn ).toBeVisible(); + } ); + } ); + it( 'should pass the correct properties to the rendered element', () => { + render( ); + mockExpressPaymentMethodNames.forEach( ( name ) => { + expect( MockEditorExpressButton ).toHaveBeenCalledWith( + getExpectedExpressPaymentProps( name ), + {} + ); + } ); + } ); + } ); + } ); +} ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/types.ts new file mode 100644 index 00000000000..af521dc3f67 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/types.ts @@ -0,0 +1,23 @@ +export type ExpressCheckoutAttributes = { + className?: string; + buttonHeight: string; + showButtonStyles: boolean; + buttonBorderRadius: string; + lock: { + move: boolean; + remove: boolean; + }; +}; + +export type ExpressCartAttributes = { + className: string; + buttonHeight: string; + showButtonStyles: boolean; + buttonBorderRadius: string; +}; + +export type ExpressPaymentSettings = { + showButtonStyles: boolean; + buttonHeight: string; + buttonBorderRadius: string; +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/block.json b/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/block.json index ab2c221618a..cba68cc4f13 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/block.json +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/block.json @@ -13,6 +13,18 @@ "lock": false }, "attributes": { + "showButtonStyles": { + "type": "boolean", + "default": false + }, + "buttonHeight": { + "type": "string", + "default": "48" + }, + "buttonBorderRadius": { + "type": "string", + "default": "4" + }, "lock": { "type": "object", "default": { diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/edit.tsx index 8bf88316bb3..db7834518c0 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/edit.tsx @@ -4,17 +4,23 @@ import { useBlockProps } from '@wordpress/block-editor'; import { useExpressPaymentMethods } from '@woocommerce/base-context/hooks'; import clsx from 'clsx'; +import { ExpressPaymentControls } from '@woocommerce/blocks/cart-checkout-shared'; +import type { BlockAttributes } from '@wordpress/blocks'; /** * Internal dependencies */ import Block from './block'; import './editor.scss'; +import type { ExpressCartAttributes } from '../../../cart-checkout-shared/types'; +import { ExpressPaymentContext } from '../../../cart-checkout-shared/payment-methods/express-payment/express-payment-context'; export const Edit = ( { attributes, + setAttributes, }: { - attributes: { className: string }; + attributes: ExpressCartAttributes; + setAttributes: ( attrs: BlockAttributes ) => void; } ): JSX.Element | null => { const { paymentMethods, isInitialized } = useExpressPaymentMethods(); const hasExpressPaymentMethods = Object.keys( paymentMethods ).length > 0; @@ -24,7 +30,9 @@ export const Edit = ( { hasExpressPaymentMethods, } ), } ); - const { className } = attributes; + + const { className, showButtonStyles, buttonHeight, buttonBorderRadius } = + attributes; if ( ! isInitialized || ! hasExpressPaymentMethods ) { return null; @@ -32,7 +40,15 @@ export const Edit = ( { return (
    - + + + +
    ); }; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/editor.scss index 24a209975d4..47ddf8126fc 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/editor.scss +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/editor.scss @@ -32,3 +32,38 @@ display: block; } } + +.express-payment-styles-notice { + margin-bottom: $gap; +} + +.express-payment-styles-beta-badge { + margin-left: $grid-unit-10; + padding: 3px $grid-unit-10; + height: $grid-unit-30; + border-radius: $radius-block-ui; + background-color: $gray-900; + color: $white; + align-items: center; + font-size: $helptext-font-size; + line-height: 1; +} + +// Disabled changing units from px for border radius control +.border-radius-control-container select { + pointer-events: none; +} + +.button-height-control + .border-radius-control-container { + margin-top: $grid-unit-30; +} + +// Center images rendered in place of buttons in the editor +.wc-block-components-express-payment { + .wc-block-components-express-payment__event-buttons { + > li { + pointer-events: none; + user-select: none; + } + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/frontend.tsx b/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/frontend.tsx index 4fc9ad2897a..c4a6edd46d0 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/frontend.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart/inner-blocks/cart-express-payment-block/frontend.tsx @@ -1,6 +1,32 @@ +/** + * External dependencies + */ +import { getValidBlockAttributes } from '@woocommerce/base-utils'; + /** * Internal dependencies */ import Block from './block'; +import { ExpressPaymentContext } from '../../../cart-checkout-shared/payment-methods/express-payment/express-payment-context'; +import metadata from './block.json'; +import { ExpressCheckoutAttributes } from '../../../cart-checkout-shared/types'; -export default Block; +const FrontendBlock = ( attributes: ExpressCheckoutAttributes ) => { + const validAttributes = getValidBlockAttributes( + metadata.attributes, + attributes + ); + + const { showButtonStyles, buttonHeight, buttonBorderRadius, className } = + validAttributes; + + return ( + + + + ); +}; + +export default FrontendBlock; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/block.json b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/block.json index cb5edb97731..7ba93490519 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/block.json +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/block.json @@ -13,6 +13,18 @@ "lock": false }, "attributes": { + "showButtonStyles": { + "type": "boolean", + "default": false + }, + "buttonHeight": { + "type": "string", + "default": "48" + }, + "buttonBorderRadius": { + "type": "string", + "default": "4" + }, "className": { "type": "string", "default": "" diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/block.tsx index cbaa8434163..f6b104d22f9 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/block.tsx @@ -10,7 +10,6 @@ import { CheckoutExpressPayment } from '../../../cart-checkout-shared/payment-me const Block = ( { className }: { className?: string } ): JSX.Element | null => { const { cartNeedsPayment } = useStoreCart(); - if ( ! cartNeedsPayment ) { return null; } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/edit.tsx index f076388889d..399f873bac1 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/edit.tsx @@ -4,23 +4,23 @@ import { useBlockProps } from '@wordpress/block-editor'; import { useExpressPaymentMethods } from '@woocommerce/base-context/hooks'; import clsx from 'clsx'; +import { ExpressPaymentControls } from '@woocommerce/blocks/cart-checkout-shared'; +import type { BlockAttributes } from '@wordpress/blocks'; /** * Internal dependencies */ import Block from './block'; import './editor.scss'; +import type { ExpressCheckoutAttributes } from '../../../cart-checkout-shared/types'; +import { ExpressPaymentContext } from '../../../cart-checkout-shared/payment-methods/express-payment/express-payment-context'; export const Edit = ( { attributes, + setAttributes, }: { - attributes: { - className?: string; - lock: { - move: boolean; - remove: boolean; - }; - }; + attributes: ExpressCheckoutAttributes; + setAttributes: ( attrs: BlockAttributes ) => void; } ): JSX.Element | null => { const { paymentMethods, isInitialized } = useExpressPaymentMethods(); const hasExpressPaymentMethods = Object.keys( paymentMethods ).length > 0; @@ -39,9 +39,19 @@ export const Edit = ( { return null; } + const { buttonHeight, buttonBorderRadius, showButtonStyles } = attributes; + return (
    - + + + +
    ); }; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/editor.scss index bc5ba4b76d2..ea77acc5400 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/editor.scss +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/editor.scss @@ -27,3 +27,38 @@ margin: 0 0 1em; } } + +.express-payment-styles-notice { + margin-bottom: $gap; +} + +.express-payment-styles-beta-badge { + margin-left: $grid-unit-10; + padding: 3px $grid-unit-10; + height: $grid-unit-30; + border-radius: $radius-block-ui; + background-color: $gray-900; + color: $white; + align-items: center; + font-size: $helptext-font-size; + line-height: 1; +} + +// Disabled changing units from px for border radius control +.border-radius-control-container select { + pointer-events: none; +} + +.button-height-control + .border-radius-control-container { + margin-top: $grid-unit-30; +} + +// Center images rendered in place of buttons in the editor +.wc-block-components-express-payment { + .wc-block-components-express-payment__event-buttons { + > li { + pointer-events: none; + user-select: none; + } + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/frontend.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/frontend.tsx new file mode 100644 index 00000000000..a458cde4131 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/frontend.tsx @@ -0,0 +1,32 @@ +/** + * External dependencies + */ +import { getValidBlockAttributes } from '@woocommerce/base-utils'; + +/** + * Internal dependencies + */ +import Block from './block'; +import { ExpressPaymentContext } from '../../../cart-checkout-shared/payment-methods/express-payment/express-payment-context'; +import metadata from './block.json'; +import { ExpressCheckoutAttributes } from '../../../cart-checkout-shared/types'; + +const FrontendBlock = ( attributes: ExpressCheckoutAttributes ) => { + const validAttributes = getValidBlockAttributes( + metadata.attributes, + attributes + ); + + const { showButtonStyles, buttonHeight, buttonBorderRadius } = + validAttributes; + + return ( + + + + ); +}; + +export default FrontendBlock; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/types.ts new file mode 100644 index 00000000000..93e998b7dd4 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-express-payment-block/types.ts @@ -0,0 +1,10 @@ +export type ExpressCheckoutAttributes = { + className?: string; + buttonHeight: string; + showButtonStyles: boolean; + buttonBorderRadius: string; + lock: { + move: boolean; + remove: boolean; + }; +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/register-components.ts b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/register-components.ts index 803344294f6..b7ed53c3d79 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/register-components.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/register-components.ts @@ -33,7 +33,7 @@ registerCheckoutBlock( { component: lazy( () => import( - /* webpackChunkName: "checkout-blocks/express-payment" */ './checkout-express-payment-block/block' + /* webpackChunkName: "checkout-blocks/express-payment" */ './checkout-express-payment-block/frontend' ) ), } ); diff --git a/plugins/woocommerce-blocks/assets/js/data/payment/test/check-payment-methods.tsx b/plugins/woocommerce-blocks/assets/js/data/payment/test/check-payment-methods.tsx index 4669d09e0a1..fda47363036 100644 --- a/plugins/woocommerce-blocks/assets/js/data/payment/test/check-payment-methods.tsx +++ b/plugins/woocommerce-blocks/assets/js/data/payment/test/check-payment-methods.tsx @@ -123,6 +123,9 @@ const registerMockPaymentMethods = ( savedCards = true ) => { }; registerExpressPaymentMethod( { name, + title: 'Express Payment Method', + description: 'A test express payment method', + gatewayId: 'test-express-payment-method', content: , edit:
    An express payment method
    , canMakePayment: mockedExpressCanMakePayment, diff --git a/plugins/woocommerce-blocks/assets/js/data/payment/test/selectors.js b/plugins/woocommerce-blocks/assets/js/data/payment/test/selectors.js index cf0739e6a95..b7d30f83a1c 100644 --- a/plugins/woocommerce-blocks/assets/js/data/payment/test/selectors.js +++ b/plugins/woocommerce-blocks/assets/js/data/payment/test/selectors.js @@ -123,6 +123,9 @@ const registerMockPaymentMethods = ( savedCards = true ) => { }; registerExpressPaymentMethod( { name, + title: `${ name } express payment method`, + description: `${ name } express payment method description`, + gatewayId: 'woo', content: , edit:
    An express payment method
    , canMakePayment: () => true, diff --git a/plugins/woocommerce-blocks/assets/js/data/payment/utils/check-payment-methods.ts b/plugins/woocommerce-blocks/assets/js/data/payment/utils/check-payment-methods.ts index 594c04fd8c2..1391b00a074 100644 --- a/plugins/woocommerce-blocks/assets/js/data/payment/utils/check-payment-methods.ts +++ b/plugins/woocommerce-blocks/assets/js/data/payment/utils/check-payment-methods.ts @@ -163,11 +163,30 @@ export const checkPaymentMethodsCanPay = async ( express = false ) => { | PaymentMethodConfigInstance | ExpressPaymentMethodConfigInstance ) => { - const { name } = paymentMethod; - availablePaymentMethods = { - ...availablePaymentMethods, - [ paymentMethod.name ]: { name }, - }; + if ( express ) { + const { name, title, description, gatewayId, supports } = + paymentMethod as ExpressPaymentMethodConfigInstance; + + availablePaymentMethods = { + ...availablePaymentMethods, + [ paymentMethod.name ]: { + name, + title, + description, + gatewayId, + supportsStyle: supports?.style, + }, + }; + } else { + const { name } = paymentMethod as PaymentMethodConfigInstance; + + availablePaymentMethods = { + ...availablePaymentMethods, + [ paymentMethod.name ]: { + name, + }, + }; + } }; // Order payment methods. diff --git a/plugins/woocommerce-blocks/assets/js/types/type-defs/payments.ts b/plugins/woocommerce-blocks/assets/js/types/type-defs/payments.ts index c0c82626ecb..2a11bf7e7fb 100644 --- a/plugins/woocommerce-blocks/assets/js/types/type-defs/payments.ts +++ b/plugins/woocommerce-blocks/assets/js/types/type-defs/payments.ts @@ -31,6 +31,7 @@ export interface SupportsConfiguration { features?: string[]; // Deprecated, in favour of showSavedCards and showSaveOption savePaymentInfo?: boolean; + style?: string[]; } // we assign a value in the class for supports.features @@ -119,10 +120,28 @@ export interface PaymentMethodConfiguration { savedTokenComponent?: ReactNode | null; } -export type ExpressPaymentMethodConfiguration = Omit< - PaymentMethodConfiguration, - 'icons' | 'label' | 'ariaLabel' | 'placeOrderButtonLabel' ->; +export interface ExpressPaymentMethodConfiguration { + // A unique string to identify the payment method client side. + name: string; + // A human readable title for the payment method. + title?: string; + // A human readable description for the payment method. + description?: string; + // The gateway ID for the payment method. + gatewayId?: string; + // A react node for your payment method UI. + content: ReactNode; + // A react node to display a preview of your payment method in the editor. + edit: ReactNode; + // A callback to determine whether the payment method should be shown in the checkout. + canMakePayment: CanMakePaymentCallback; + // A unique string to represent the payment method server side. If not provided, defaults to name. + paymentMethodId?: string; + // Object that describes various features provided by the payment method. + supports: SupportsConfiguration; + // A React node that contains logic handling any processing your payment method has to do with saved payment methods if your payment method supports them + savedTokenComponent?: ReactNode | null; +} export type PaymentMethods = | Record< string, PaymentMethodConfigInstance > @@ -131,7 +150,16 @@ export type PaymentMethods = /** * Used to represent payment methods in a context where storing objects is not allowed, i.e. in data stores. */ -export type PlainPaymentMethods = Record< string, { name: string } >; +export type PlainPaymentMethods = Record< + string, + { + name: string; + title: string; + description: string; + gatewayId: string; + supportsStyle: string[]; + } +>; /** * Used to represent payment methods in a context where storing objects is not allowed, i.e. in data stores. @@ -159,6 +187,9 @@ export interface PaymentMethodConfigInstance { export interface ExpressPaymentMethodConfigInstance { name: string; + title: string; + description: string; + gatewayId: string; content: ReactNode; edit: ReactNode; paymentMethodId?: string; diff --git a/plugins/woocommerce/changelog/47899-try-poc-express-checkout-button-styles b/plugins/woocommerce/changelog/47899-try-poc-express-checkout-button-styles new file mode 100644 index 00000000000..857d00e2bb8 --- /dev/null +++ b/plugins/woocommerce/changelog/47899-try-poc-express-checkout-button-styles @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Adds unified styles for the express checkout block \ No newline at end of file diff --git a/plugins/woocommerce/changelog/50688-add-sync-cart-checkout b/plugins/woocommerce/changelog/50688-add-sync-cart-checkout new file mode 100644 index 00000000000..2358e0fc1ce --- /dev/null +++ b/plugins/woocommerce/changelog/50688-add-sync-cart-checkout @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Synchronise the express payment controls between the Cart & Checkout blocks \ No newline at end of file diff --git a/plugins/woocommerce/changelog/50791-feature-express-payment-improvements b/plugins/woocommerce/changelog/50791-feature-express-payment-improvements new file mode 100644 index 00000000000..84c3df51562 --- /dev/null +++ b/plugins/woocommerce/changelog/50791-feature-express-payment-improvements @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Improve the express checkout experience with several design tweak, uniform button styles and editor improvements \ No newline at end of file diff --git a/plugins/woocommerce/changelog/50983-add-express-payment-methods-to-sidebar b/plugins/woocommerce/changelog/50983-add-express-payment-methods-to-sidebar new file mode 100644 index 00000000000..640c491e1cc --- /dev/null +++ b/plugins/woocommerce/changelog/50983-add-express-payment-methods-to-sidebar @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Available express payment methods are visible in the editor when selecting the express payment block \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51109-remove-dark-mode b/plugins/woocommerce/changelog/51109-remove-dark-mode new file mode 100644 index 00000000000..b0cb81d045a --- /dev/null +++ b/plugins/woocommerce/changelog/51109-remove-dark-mode @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak +Comment: This is part of a bigger feature that will have its own changelog entry + diff --git a/plugins/woocommerce/changelog/51296-try-supports-express-payment-controls b/plugins/woocommerce/changelog/51296-try-supports-express-payment-controls new file mode 100644 index 00000000000..ef5d03c75bb --- /dev/null +++ b/plugins/woocommerce/changelog/51296-try-supports-express-payment-controls @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +UX improvements to the express payment block in the editor \ No newline at end of file diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/CartExpressPaymentBlock.php b/plugins/woocommerce/src/Blocks/BlockTypes/CartExpressPaymentBlock.php index d491967c844..b8fd43c7b5b 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/CartExpressPaymentBlock.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/CartExpressPaymentBlock.php @@ -11,4 +11,18 @@ class CartExpressPaymentBlock extends AbstractInnerBlock { * @var string */ protected $block_name = 'cart-express-payment-block'; + + /** + * Uniform default_styles for the express payment buttons + * + * @var boolean + */ + protected $default_styles = null; + + /** + * Current styles for the express payment buttons + * + * @var boolean + */ + protected $current_styles = null; } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutExpressPaymentBlock.php b/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutExpressPaymentBlock.php index 3efe4261e6a..a5864d7502c 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutExpressPaymentBlock.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutExpressPaymentBlock.php @@ -1,6 +1,9 @@ default_styles = array( + 'showButtonStyles' => false, + 'buttonHeight' => '48', + 'buttonBorderRadius' => '4', + ); + + add_action( 'save_post', array( $this, 'sync_express_payment_attrs' ), 10, 2 ); + } + + /** + * Synchorize the express payment attributes between the Cart and Checkout pages. + * + * @param int $post_id Post ID. + * @param WP_Post $post Post object. + */ + public function sync_express_payment_attrs( $post_id, $post ) { + if ( wc_get_page_id( 'cart' ) === $post_id ) { + $cart_or_checkout = 'cart'; + } elseif ( wc_get_page_id( 'checkout' ) === $post_id ) { + $cart_or_checkout = 'checkout'; + } else { + return; + } + + // This is not a proper save action, maybe an autosave, so don't continue. + if ( empty( $post->post_status ) || 'inherit' === $post->post_status ) { + return; + } + + $block_name = 'woocommerce/' . $cart_or_checkout; + $page_id = 'woocommerce_' . $cart_or_checkout . '_page_id'; + $template_name = 'page-' . $cart_or_checkout; + + // Check if we are editing the cart/checkout page and that it contains a Cart/Checkout block. + // Cast to string for Cart/Checkout page ID comparison because get_option can return it as a string, so better to compare both values as strings. + if ( ! empty( $post->post_type ) && 'wp_template' !== $post->post_type && ( false === has_block( $block_name, $post ) || (string) get_option( $page_id ) !== (string) $post_id ) ) { + return; + } + + // Check if we are editing the Cart/Checkout template and that it contains a Cart/Checkout block. + if ( ( ! empty( $post->post_type ) && ! empty( $post->post_name ) && $template_name !== $post->post_name && 'wp_template' === $post->post_type ) || false === has_block( $block_name, $post ) ) { + return; + } + + if ( empty( $post->post_content ) ) { + return; + } + + try { + // Parse the post content to get the express payment attributes of the current page. + $blocks = parse_blocks( $post->post_content ); + $attrs = CartCheckoutUtils::find_express_checkout_attributes( $blocks, $cart_or_checkout ); + + if ( ! is_array( $attrs ) ) { + return; + } + $updated_attrs = array_merge( $this->default_styles, $attrs ); + + // We need to sync the attributes between the Cart and Checkout pages. + $other_page = 'cart' === $cart_or_checkout ? 'checkout' : 'cart'; + + $this->update_other_page_with_express_payment_attrs( $other_page, $updated_attrs ); + } catch ( Exception $e ) { + wc_get_logger()->log( 'error', 'Error updating express payment attributes: ' . $e->getMessage() ); + } + } + + /** + * Update the express payment attributes in the other page (Cart or Checkout). + * + * @param string $cart_or_checkout The page to update. + * @param array $updated_attrs The updated attributes. + */ + private function update_other_page_with_express_payment_attrs( $cart_or_checkout, $updated_attrs ) { + $page_id = 'cart' === $cart_or_checkout ? wc_get_page_id( 'cart' ) : wc_get_page_id( 'checkout' ); + + if ( -1 === $page_id ) { + return; + } + + $post = get_post( $page_id ); + + if ( empty( $post->post_content ) ) { + return; + } + + $blocks = parse_blocks( $post->post_content ); + CartCheckoutUtils::update_blocks_with_new_attrs( $blocks, $cart_or_checkout, $updated_attrs ); + + $updated_content = serialize_blocks( $blocks ); + remove_action( 'save_post', array( $this, 'sync_express_payment_attrs' ), 10, 2 ); + + wp_update_post( + array( + 'ID' => $page_id, + 'post_content' => $updated_content, + ), + false, + false + ); + + add_action( 'save_post', array( $this, 'sync_express_payment_attrs' ), 10, 2 ); + } } diff --git a/plugins/woocommerce/src/Blocks/Utils/BlockTemplateUtils.php b/plugins/woocommerce/src/Blocks/Utils/BlockTemplateUtils.php index 595ceca323e..8e6133155fa 100644 --- a/plugins/woocommerce/src/Blocks/Utils/BlockTemplateUtils.php +++ b/plugins/woocommerce/src/Blocks/Utils/BlockTemplateUtils.php @@ -481,7 +481,7 @@ class BlockTemplateUtils { * @return boolean */ public static function theme_has_template( $template_name ) { - return ! ! self::get_theme_template_path( $template_name, 'wp_template' ); + return (bool) self::get_theme_template_path( $template_name, 'wp_template' ); } /** @@ -491,7 +491,7 @@ class BlockTemplateUtils { * @return boolean */ public static function theme_has_template_part( $template_name ) { - return ! ! self::get_theme_template_path( $template_name, 'wp_template_part' ); + return (bool) self::get_theme_template_path( $template_name, 'wp_template_part' ); } /** diff --git a/plugins/woocommerce/src/Blocks/Utils/CartCheckoutUtils.php b/plugins/woocommerce/src/Blocks/Utils/CartCheckoutUtils.php index 53b4cde7f56..8b9e0e9098a 100644 --- a/plugins/woocommerce/src/Blocks/Utils/CartCheckoutUtils.php +++ b/plugins/woocommerce/src/Blocks/Utils/CartCheckoutUtils.php @@ -108,7 +108,7 @@ class CartCheckoutUtils { } $array_without_accents = array_map( - function( $value ) { + function ( $value ) { return is_array( $value ) ? self::deep_sort_with_accents( $value ) : remove_accents( wc_strtolower( html_entity_decode( $value ) ) ); @@ -129,7 +129,7 @@ class CartCheckoutUtils { $shipping_zones = \WC_Shipping_Zones::get_zones(); $formatted_shipping_zones = array_reduce( $shipping_zones, - function( $acc, $zone ) { + function ( $acc, $zone ) { $acc[] = [ 'id' => $zone['id'], 'title' => $zone['zone_name'], @@ -146,4 +146,47 @@ class CartCheckoutUtils { ]; return $formatted_shipping_zones; } + + /** + * Recursively search the checkout block to find the express checkout block and + * get the button style attributes + * + * @param array $blocks Blocks to search. + * @param string $cart_or_checkout The block type to check. + */ + public static function find_express_checkout_attributes( $blocks, $cart_or_checkout ) { + $express_block_name = 'woocommerce/' . $cart_or_checkout . '-express-payment-block'; + foreach ( $blocks as $block ) { + if ( ! empty( $block['blockName'] ) && $express_block_name === $block['blockName'] && ! empty( $block['attrs'] ) ) { + return $block['attrs']; + } + + if ( ! empty( $block['innerBlocks'] ) ) { + $answer = self::find_express_checkout_attributes( $block['innerBlocks'], $cart_or_checkout ); + if ( $answer ) { + return $answer; + } + } + } + } + + /** + * Given an array of blocks, find the express payment block and update its attributes. + * + * @param array $blocks Blocks to search. + * @param string $cart_or_checkout The block type to check. + * @param array $updated_attrs The new attributes to set. + */ + public static function update_blocks_with_new_attrs( &$blocks, $cart_or_checkout, $updated_attrs ) { + $express_block_name = 'woocommerce/' . $cart_or_checkout . '-express-payment-block'; + foreach ( $blocks as $key => &$block ) { + if ( ! empty( $block['blockName'] ) && $express_block_name === $block['blockName'] ) { + $blocks[ $key ]['attrs'] = $updated_attrs; + } + + if ( ! empty( $block['innerBlocks'] ) ) { + self::update_blocks_with_new_attrs( $block['innerBlocks'], $cart_or_checkout, $updated_attrs ); + } + } + } } From 34aa4f07f19c87dd5780d241929d67c3c2560416 Mon Sep 17 00:00:00 2001 From: Barry Hughes <3594411+barryhughes@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:06:54 -0700 Subject: [PATCH 13/20] Update stable tag to 9.3.2 (#51522) Update stable tag to 9.3.2 --- plugins/woocommerce/readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/readme.txt b/plugins/woocommerce/readme.txt index d3730c2db5b..6b77c072437 100644 --- a/plugins/woocommerce/readme.txt +++ b/plugins/woocommerce/readme.txt @@ -4,7 +4,7 @@ Tags: online store, ecommerce, shop, shopping cart, sell online Requires at least: 6.5 Tested up to: 6.6 Requires PHP: 7.4 -Stable tag: 9.2.3 +Stable tag: 9.3.2 License: GPLv3 License URI: https://www.gnu.org/licenses/gpl-3.0.html From 8cdefdcd01479ca22eab8c6c779e03aa648b7ddb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:13:18 -0700 Subject: [PATCH 14/20] Update changelog.txt from release 9.3.2 (#51523) Prep trunk post release 9.3.2 Co-authored-by: WooCommerce Bot --- changelog.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/changelog.txt b/changelog.txt index 2ba165e2bdc..592a7f919cf 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,12 @@ == Changelog == += 9.3.2 2024-09-18 = + +- Fix - Improve the product importer's handling of filepaths under Windows [#51456](https://github.com/woocommerce/woocommerce/pull/51456) +- Fix - Revert changes related to low stock product notifications [#51441](https://github.com/woocommerce/woocommerce/pull/51441) +- Fix - Resolve a bug where manually triggering `added_to_cart` event without a button element caused an Exception [#51449](https://github.com/woocommerce/woocommerce/pull/51449) + + = 9.3.1 2024-09-12 = * Tweak - Disable remote logging feature by default [#51312](https://github.com/woocommerce/woocommerce/pull/51312) From 2fc6528c619ca0c816f60a699b10867e743f9afa Mon Sep 17 00:00:00 2001 From: DAnn2012 Date: Thu, 19 Sep 2024 05:14:36 +0200 Subject: [PATCH 15/20] Fix typo (task-list-completed.tsx) (#51484) * Update task-list-completed.tsx * Add changefile(s) from automation for the following project(s): woocommerce --------- Co-authored-by: github-actions Co-authored-by: Chi-Hsuan Huang --- .../setup-task-list/components/task-list-completed.tsx | 2 +- plugins/woocommerce/changelog/51484-patch-25 | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/51484-patch-25 diff --git a/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-list-completed.tsx b/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-list-completed.tsx index a91359b9b4c..4c088020691 100644 --- a/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-list-completed.tsx +++ b/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-list-completed.tsx @@ -29,7 +29,7 @@ export const TaskListCompleted = ( { className="woocommerce-task-card woocommerce-homescreen-card completed" > -
    +
    Completed

    { __( diff --git a/plugins/woocommerce/changelog/51484-patch-25 b/plugins/woocommerce/changelog/51484-patch-25 new file mode 100644 index 00000000000..28eacb4af87 --- /dev/null +++ b/plugins/woocommerce/changelog/51484-patch-25 @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak +Comment: Fix typo (task-list-completed.tsx) + From dfd7d52d6b51af139bac062c8977c3444295da29 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Thu, 19 Sep 2024 12:22:53 +0800 Subject: [PATCH 16/20] [beta tester] Simulate core error early in the request lifecycle before WP fully loaded. (#51330) * Improve remote logging tool to simulate core error early in the request lifecycle before wp fully loaded * Disable wp error handler when simulate_woocommerce_error --- .../dev-simulate-error-before-wp-loaded | 4 +++ .../woocommerce-beta-tester.php | 27 ++++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 plugins/woocommerce-beta-tester/changelog/dev-simulate-error-before-wp-loaded diff --git a/plugins/woocommerce-beta-tester/changelog/dev-simulate-error-before-wp-loaded b/plugins/woocommerce-beta-tester/changelog/dev-simulate-error-before-wp-loaded new file mode 100644 index 00000000000..874acda860a --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/dev-simulate-error-before-wp-loaded @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Improve remote logging tool to simulate core error early in the request lifecycle before wp fully loaded diff --git a/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php b/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php index bc1d9a76d72..f6bdbf24e8d 100644 --- a/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php +++ b/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php @@ -143,10 +143,30 @@ add_action( /** * Simulate a WooCommerce error for remote logging testing. * - * @throws Exception A simulated WooCommerce error if the option is set. + * This function adds a filter to the 'woocommerce_template_path' hook + * that throws an exception, then triggers the filter by calling WC()->template_path(). + * + * @throws Exception A simulated WooCommerce error for testing purposes. */ function simulate_woocommerce_error() { - throw new Exception( 'Simulated WooCommerce error for remote logging test' ); + // Return if WooCommerce is not loaded. + if ( ! function_exists( 'WC' ) || ! class_exists( 'WooCommerce' ) ) { + return; + } + + // Define a constant to prevent the error from being caught by the WP Error Handler. + if ( ! defined( 'WP_SANDBOX_SCRAPING' ) ) { + define( 'WP_SANDBOX_SCRAPING', true ); + } + + add_filter( + 'woocommerce_template_path', + function() { + throw new Exception( 'Simulated WooCommerce error for remote logging test' ); + } + ); + + WC()->template_path(); } $simulate_error = get_option( 'wc_beta_tester_simulate_woocommerce_php_error', false ); @@ -155,7 +175,8 @@ if ( $simulate_error ) { delete_option( 'wc_beta_tester_simulate_woocommerce_php_error' ); if ( 'core' === $simulate_error ) { - add_action( 'woocommerce_loaded', 'simulate_woocommerce_error' ); + // Hook into the plugin_loaded action to simulate the error early before WP fully initializes. + add_action( 'plugin_loaded', 'simulate_woocommerce_error' ); } elseif ( 'beta-tester' === $simulate_error ) { throw new Exception( 'Test PHP exception from WooCommerce Beta Tester' ); } From 2e69e9402865868aa597eee04eb1d5215411f621 Mon Sep 17 00:00:00 2001 From: Jonathan Lane Date: Thu, 19 Sep 2024 00:02:04 -0700 Subject: [PATCH 17/20] Exclude skipped e2e tests from auto-closure (#51528) Co-authored-by: Jon Lane --- .github/workflows/stalebot.yml | 93 ++++++++++--------- ...2e-fix-stalebot-ignore-skipped-test-issues | 4 + 2 files changed, 51 insertions(+), 46 deletions(-) create mode 100644 plugins/woocommerce/changelog/e2e-fix-stalebot-ignore-skipped-test-issues diff --git a/.github/workflows/stalebot.yml b/.github/workflows/stalebot.yml index 669c3f09dfc..5f7ab100c63 100644 --- a/.github/workflows/stalebot.yml +++ b/.github/workflows/stalebot.yml @@ -1,51 +1,52 @@ name: 'Process stale needs-feedback issues' on: - schedule: - - cron: '21 0 * * *' - workflow_dispatch: + schedule: + - cron: '21 0 * * *' + workflow_dispatch: -permissions: { } +permissions: {} jobs: - stale: - runs-on: ubuntu-20.04 - permissions: - contents: read - issues: write - pull-requests: write - steps: - - name: Scan issues - uses: actions/stale@v9.0.0 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-issue-message: "As a part of this repository's maintenance, this issue is being marked as stale due to inactivity. Please feel free to comment on it in case we missed something.\n\n###### After 7 days with no activity this issue will be automatically be closed." - close-issue-message: 'This issue was closed because it has been 14 days with no activity.' - operations-per-run: 140 - days-before-stale: -1 - days-before-close: -1 - days-before-issue-stale: 7 - days-before-issue-close: 7 - stale-issue-label: 'status: stale' - stale-pr-label: 'status: stale' - exempt-issue-labels: 'type: enhancement' - only-issue-labels: 'needs: author feedback' - close-issue-label: "status: can't reproduce" - ascending: true - - name: Process Stale Flaky Test Issues - uses: actions/stale@v9.0.0 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - only-issue-labels: 'metric: flaky e2e test' - days-before-stale: -1 - days-before-close: -1 - days-before-issue-stale: 5 - days-before-issue-close: 2 - stale-issue-label: 'status: stale' - stale-issue-message: 'This issue is being marked as stale due to inactivity. It will be auto-closed if no further activity occurs within the next 2 days.' - close-issue-message: 'Auto-closed due to inactivity. Please re-open if you believe this issue is still valid.' - close-issue-reason: 'not_planned' - remove-stale-when-updated: true - exempt-all-assignees: false - enable-statistics: true - ascending: true - operations-per-run: 120 + stale: + runs-on: ubuntu-20.04 + permissions: + contents: read + issues: write + pull-requests: write + steps: + - name: Scan issues + uses: actions/stale@v9.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: "As a part of this repository's maintenance, this issue is being marked as stale due to inactivity. Please feel free to comment on it in case we missed something.\n\n###### After 7 days with no activity this issue will be automatically be closed." + close-issue-message: 'This issue was closed because it has been 14 days with no activity.' + operations-per-run: 140 + days-before-stale: -1 + days-before-close: -1 + days-before-issue-stale: 7 + days-before-issue-close: 7 + stale-issue-label: 'status: stale' + stale-pr-label: 'status: stale' + exempt-issue-labels: 'type: enhancement' + only-issue-labels: 'needs: author feedback' + close-issue-label: "status: can't reproduce" + ascending: true + - name: Process Stale Flaky Test Issues + uses: actions/stale@v9.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + only-issue-labels: 'metric: flaky e2e test' + exempt-issue-labels: 'metric: skipped test' + days-before-stale: -1 + days-before-close: -1 + days-before-issue-stale: 5 + days-before-issue-close: 2 + stale-issue-label: 'status: stale' + stale-issue-message: 'This issue is being marked as stale due to inactivity. It will be auto-closed if no further activity occurs within the next 2 days.' + close-issue-message: 'Auto-closed due to inactivity. Please re-open if you believe this issue is still valid.' + close-issue-reason: 'not_planned' + remove-stale-when-updated: true + exempt-all-assignees: false + enable-statistics: true + ascending: true + operations-per-run: 120 diff --git a/plugins/woocommerce/changelog/e2e-fix-stalebot-ignore-skipped-test-issues b/plugins/woocommerce/changelog/e2e-fix-stalebot-ignore-skipped-test-issues new file mode 100644 index 00000000000..77cf3c60d67 --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-fix-stalebot-ignore-skipped-test-issues @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Exclude skipped e2e test issues from stale bot closure From b0799716aa1f46c4229bec50bfec81f3fb52d8a2 Mon Sep 17 00:00:00 2001 From: Vladimir Reznichenko Date: Thu, 19 Sep 2024 10:33:57 +0200 Subject: [PATCH 18/20] [dev] Monorepo: bypass git clone for WordPress core when spinning off wp-env (#51457) --- plugins/woocommerce-blocks/.wp-env.json | 2 +- plugins/woocommerce/.wp-env.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-blocks/.wp-env.json b/plugins/woocommerce-blocks/.wp-env.json index 49edf6823e2..35e6e393ad6 100644 --- a/plugins/woocommerce-blocks/.wp-env.json +++ b/plugins/woocommerce-blocks/.wp-env.json @@ -1,5 +1,5 @@ { - "core": null, + "core": "https://wordpress.org/wordpress-latest.zip", "phpVersion": "7.4", "plugins": [ "https://github.com/WP-API/Basic-Auth/archive/master.zip", diff --git a/plugins/woocommerce/.wp-env.json b/plugins/woocommerce/.wp-env.json index 333f1fdd26a..70f9783578d 100644 --- a/plugins/woocommerce/.wp-env.json +++ b/plugins/woocommerce/.wp-env.json @@ -1,5 +1,5 @@ { - "core": null, + "core": "https://wordpress.org/wordpress-latest.zip", "phpVersion": "7.4", "plugins": [ "." ], "config": { From 11b1e31ac373e55cbc8ffd99a4f23ff06f5b199a Mon Sep 17 00:00:00 2001 From: louwie17 Date: Thu, 19 Sep 2024 05:43:05 -0300 Subject: [PATCH 19/20] Update the dataviews package dependency (#51511) * Initial update * Revert dependency output change * Add changelogs --- .../changelog/update-dataviews_package | 4 + packages/js/product-editor/package.json | 2 +- packages/js/product-editor/src/products.scss | 10 +- .../changelog/update-dataviews_package | 4 + .../Admin/Features/ProductDataViews/Init.php | 4 +- pnpm-lock.yaml | 901 ++++++++++-------- 6 files changed, 531 insertions(+), 394 deletions(-) create mode 100644 packages/js/product-editor/changelog/update-dataviews_package create mode 100644 plugins/woocommerce/changelog/update-dataviews_package diff --git a/packages/js/product-editor/changelog/update-dataviews_package b/packages/js/product-editor/changelog/update-dataviews_package new file mode 100644 index 00000000000..c1eb32092ed --- /dev/null +++ b/packages/js/product-editor/changelog/update-dataviews_package @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update @wordpress/dataviews package and fix class names for products dataviews app. diff --git a/packages/js/product-editor/package.json b/packages/js/product-editor/package.json index 52ec6eacb88..7ec2d530d27 100644 --- a/packages/js/product-editor/package.json +++ b/packages/js/product-editor/package.json @@ -56,7 +56,7 @@ "@wordpress/compose": "wp-6.0", "@wordpress/core-data": "wp-6.0", "@wordpress/data": "wp-6.0", - "@wordpress/dataviews": "^4.2.0", + "@wordpress/dataviews": "^4.3.0", "@wordpress/date": "wp-6.0", "@wordpress/deprecated": "wp-6.0", "@wordpress/edit-post": "wp-6.0", diff --git a/packages/js/product-editor/src/products.scss b/packages/js/product-editor/src/products.scss index 463564ff88e..73b5e0afd57 100644 --- a/packages/js/product-editor/src/products.scss +++ b/packages/js/product-editor/src/products.scss @@ -1,15 +1,15 @@ @include wordpress-admin-schemes(); -.woocommerce_page_woocommerce-products-dashboard #wpadminbar, -.woocommerce_page_woocommerce-products-dashboard #adminmenumain { +.product_page_woocommerce-products-dashboard #wpadminbar, +.product_page_woocommerce-products-dashboard #adminmenumain { display: none; } -.woocommerce_page_woocommerce-products-dashboard #wpcontent { +.product_page_woocommerce-products-dashboard #wpcontent { margin-left: 0; } -body.woocommerce_page_woocommerce-products-dashboard #woocommerce-products-dashboard { +body.product_page_woocommerce-products-dashboard #woocommerce-products-dashboard { @include wp-admin-reset("#woocommerce-products-dashboard"); @include reset; display: block !important; @@ -40,7 +40,7 @@ body.js.is-fullscreen-mode { } } -.woocommerce_page_woocommerce-products-dashboard { +.product_page_woocommerce-products-dashboard { @import "products-app/sidebar-dataviews/style.scss"; @import "products-app/product-edit/style.scss"; } diff --git a/plugins/woocommerce/changelog/update-dataviews_package b/plugins/woocommerce/changelog/update-dataviews_package new file mode 100644 index 00000000000..1df54ed175a --- /dev/null +++ b/plugins/woocommerce/changelog/update-dataviews_package @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +[ Experimental ] Moved experimental product dataviews menu to Product top menu. diff --git a/plugins/woocommerce/src/Admin/Features/ProductDataViews/Init.php b/plugins/woocommerce/src/Admin/Features/ProductDataViews/Init.php index 26909eff3ce..8c445e9bc2f 100644 --- a/plugins/woocommerce/src/Admin/Features/ProductDataViews/Init.php +++ b/plugins/woocommerce/src/Admin/Features/ProductDataViews/Init.php @@ -109,9 +109,9 @@ class Init { } $ptype_obj = get_post_type_object( 'product' ); add_submenu_page( - 'woocommerce', + 'edit.php?post_type=product', $ptype_obj->labels->name, - esc_html__( 'All Products', 'woocommerce' ), + esc_html__( 'All Products ( new )', 'woocommerce' ), 'manage_woocommerce', 'woocommerce-products-dashboard', array( $this, 'woocommerce_products_dashboard' ), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2f8a2a6aab..e627ba37945 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -256,7 +256,7 @@ importers: version: 10.5.0(sass@1.69.5)(webpack@5.89.0(webpack-cli@3.3.12)) ts-jest: specifier: ~29.1.1 - version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) + version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) typescript: specifier: ^5.3.3 version: 5.3.3 @@ -292,7 +292,7 @@ importers: version: 2.3.2 debug: specifier: ^4.3.4 - version: 4.3.4(supports-color@8.1.1) + version: 4.3.4(supports-color@9.4.0) dompurify: specifier: ^2.4.7 version: 2.4.7 @@ -453,7 +453,7 @@ importers: version: 27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)) ts-jest: specifier: ~29.1.1 - version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) + version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) typescript: specifier: ^5.3.3 version: 5.3.3 @@ -1040,7 +1040,7 @@ importers: version: link:../experimental '@wordpress/components': specifier: wp-6.0 - version: 19.8.5(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react-with-direction@1.4.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2) + version: 19.8.5(react-dom@17.0.2(react@17.0.2))(react-with-direction@1.4.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2) '@wordpress/compose': specifier: wp-6.0 version: 5.4.1(react@17.0.2) @@ -1717,7 +1717,7 @@ importers: version: link:../components '@wordpress/components': specifier: ^19.17.0 - version: 19.17.0(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react-with-direction@1.4.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2) + version: 19.17.0(react-dom@17.0.2(react@17.0.2))(react-with-direction@1.4.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2) '@wordpress/element': specifier: ^4.20.0 version: 4.20.0 @@ -2218,7 +2218,7 @@ importers: version: 6.3.1 '@wordpress/components': specifier: wp-6.0 - version: 19.8.5(@types/react@17.0.71)(react-dom@18.3.1(react@17.0.2))(react-with-direction@1.4.0(react-dom@18.3.1(react@17.0.2))(react@17.0.2))(react@17.0.2) + version: 19.8.5(react-dom@18.3.1(react@17.0.2))(react-with-direction@1.4.0(react-dom@18.3.1(react@17.0.2))(react@17.0.2))(react@17.0.2) '@wordpress/compose': specifier: wp-6.0 version: 5.4.1(react@17.0.2) @@ -2590,8 +2590,8 @@ importers: specifier: wp-6.0 version: 6.6.1(react@17.0.2) '@wordpress/dataviews': - specifier: ^4.2.0 - version: 4.2.0(@emotion/is-prop-valid@1.2.1)(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + specifier: ^4.3.0 + version: 4.3.0(@emotion/is-prop-valid@1.2.1)(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@wordpress/date': specifier: wp-6.0 version: 4.6.1 @@ -2866,7 +2866,7 @@ importers: dependencies: debug: specifier: ^4.3.4 - version: 4.3.4(supports-color@8.1.1) + version: 4.3.4(supports-color@9.4.0) devDependencies: '@babel/core': specifier: ^7.23.5 @@ -2975,7 +2975,7 @@ importers: version: 1.2.5(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react-with-direction@1.4.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2) debug: specifier: ^4.3.4 - version: 4.3.4(supports-color@8.1.1) + version: 4.3.4(supports-color@9.4.0) prop-types: specifier: ^15.8.1 version: 15.8.1 @@ -2991,10 +2991,10 @@ importers: devDependencies: '@babel/preset-react': specifier: 7.23.3 - version: 7.23.3(@babel/core@7.24.7) + version: 7.23.3(@babel/core@7.25.2) '@babel/preset-typescript': specifier: 7.23.2 - version: 7.23.2(@babel/core@7.24.7) + version: 7.23.2(@babel/core@7.25.2) '@svgr/webpack': specifier: ^8.1.0 version: 8.1.0(typescript@5.3.3) @@ -3045,10 +3045,10 @@ importers: version: 2.17.0(wp-prettier@2.8.5) '@wordpress/scripts': specifier: ^19.2.4 - version: 19.2.4(@babel/core@7.24.7)(debug@4.3.4)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4) + version: 19.2.4(@babel/core@7.25.2)(debug@4.3.4)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4) babel-jest: specifier: ~27.5.1 - version: 27.5.1(@babel/core@7.24.7) + version: 27.5.1(@babel/core@7.25.2) eslint: specifier: ^8.55.0 version: 8.55.0 @@ -3352,7 +3352,7 @@ importers: version: 3.34.0 debug: specifier: ^4.3.4 - version: 4.3.4(supports-color@8.1.1) + version: 4.3.4(supports-color@9.4.0) dompurify: specifier: ^2.4.7 version: 2.4.7 @@ -3906,7 +3906,7 @@ importers: version: 2.17.0(wp-prettier@2.8.5) '@wordpress/scripts': specifier: ^19.2.4 - version: 19.2.4(@babel/core@7.25.2)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4) + version: 19.2.4(@babel/core@7.25.2)(debug@4.3.4)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4) eslint: specifier: ^8.55.0 version: 8.55.0 @@ -4642,7 +4642,7 @@ importers: version: 27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)) ts-jest: specifier: ~29.1.1 - version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) + version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3) @@ -4839,7 +4839,7 @@ importers: version: 1.2.2 ts-jest: specifier: ~29.1.1 - version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) + version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) ts-loader: specifier: ^9.5.1 version: 9.5.1(typescript@5.3.3)(webpack@5.89.0(webpack-cli@3.3.12)) @@ -10570,8 +10570,8 @@ packages: resolution: {integrity: sha512-mOQtwpY5hUt4vMLyshZPPV1x9MBRF2FimUjIImfYJb1x8o6jY4npikzWplAfWYQUJJjWfw/1NmfqD7vUOh9+ww==} engines: {node: '>=12'} - '@wordpress/a11y@4.6.0': - resolution: {integrity: sha512-dSYGLgntqQCAiHBnNxttLOUZnH26m/BrIQdCXtb9JVJy5p68JAdFHbr6qFoOfOoTCvwUqE8cNS7K4GWfAJwT0w==} + '@wordpress/a11y@4.7.0': + resolution: {integrity: sha512-qeh8TcJNNr9M0XL3OUDawBRrZypNLsnLjcXEBd6jp8Y4kOWxowmDDT6re1uToPdYTLLW2PZmZeBLYR9OS7pgpw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} '@wordpress/api-fetch@3.23.1': @@ -10829,8 +10829,8 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 - '@wordpress/components@28.6.0': - resolution: {integrity: sha512-9YmA+7Tmz19oOfKifOF/VxcwJwyyLK8Y2LupK7ge6Oue0P1bMLs/9LBgZUBizoKMWmXYdzBm8pXf9Eyqq3PG0Q==} + '@wordpress/components@28.7.0': + resolution: {integrity: sha512-oxF+pAZHJ3L9p42wMDeclo6P0TOZW1+U1pKmKju33aDsAwINOU2ELpVFyIEHkA9txn8VU4lxpnNIsYY6RGlW8w==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} peerDependencies: react: ^18.0.0 @@ -10879,6 +10879,12 @@ packages: peerDependencies: react: ^18.0.0 + '@wordpress/compose@7.7.0': + resolution: {integrity: sha512-TjhGcw9n/XbiMT63POESs1TF9O6eQRVhAPrMan5t2yusQbog5KLk4TetOasIWxD80pu5sg9P5NuupuU/oSEBYQ==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + '@wordpress/core-commands@0.7.0': resolution: {integrity: sha512-kMfyANcDUmA2+4EfEZuDVNFOWKEOJe7oEaZtC6tFRR1wYAlPYOzaQJxbtQMBzqhvHlQMORaxDQNhaoJ8+ac8MQ==} engines: {node: '>=12'} @@ -10935,6 +10941,12 @@ packages: peerDependencies: react: ^18.0.0 + '@wordpress/data@10.7.0': + resolution: {integrity: sha512-0NqDYIMOHdilSYoH6LRaq1CHcWlJiGP6xxkjI6pu2ZEf5mo9S/UblLCzVwaZMnhae/ZxEsgQQIypIQJJqor9uw==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + '@wordpress/data@4.27.3': resolution: {integrity: sha512-5763NgNV9IIa1CC3Q80dAvrH6108tJtj3IrHfUCZmUk1atSNsOMBCkLdQ7tGTTi2JFejeGEMg1LJI22JD5zM6Q==} @@ -10974,8 +10986,8 @@ packages: peerDependencies: react: ^18.0.0 - '@wordpress/dataviews@4.2.0': - resolution: {integrity: sha512-rCnMbEVXKZYgQmJO7S448KPVh78DTHgfJ+B5H937l/HX8+Gd0OlkpbKi4C4UZUj0k/xwY7ccKERYurq3W8/NFg==} + '@wordpress/dataviews@4.3.0': + resolution: {integrity: sha512-ZAKgac4sFINnAKABhf4/0qAEm1LTTRDJGd8+qfi6mbiQr42BoNVIKtFw8o5uOL4cOEF01wdoUDaCZv6oP9e+eA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} peerDependencies: react: ^18.0.0 @@ -10992,8 +11004,8 @@ packages: resolution: {integrity: sha512-7/w2pzCDvzbidqAl2Rhd/FeA6QZhZmb03Y7rPIO0eJR33L8QWnLiyw+r4Et2DLji8A7N8/gcc+hsRL6lcEsGMA==} engines: {node: '>=12'} - '@wordpress/date@5.6.0': - resolution: {integrity: sha512-uB/FaNHudbs4DgaPGld+Ckvoo8kYvxcDhVyJ6Io3MgONMcsDr4KR3lOc50MprbNZPbXG2KB0CTgHA+PHNxP9iQ==} + '@wordpress/date@5.7.0': + resolution: {integrity: sha512-iMwGP/Sbz+CCgqxUUKg8W2sZiwvr9K1q7s0rHuy3YVJT46QDNpN0A6HGNmckI0z4C+CRDvOIa09OMgTz1igUAw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} '@wordpress/dependency-extraction-webpack-plugin@2.9.0': @@ -11036,6 +11048,10 @@ packages: resolution: {integrity: sha512-XQbF7SIb43I4Ey7nEDqowm7YJgzoUpdmZfNBN01/UXKUZ0FNaKzf2LCNjOCwfEfRE7AroyUgMR40qWVBBs+GKQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/deprecated@4.7.0': + resolution: {integrity: sha512-FMtYPk+yxvEAs1LDBHk1yHz48vlp/TWrTQeMph5ov7dpw4Xgfd9darXorsl4zudJVrB+Nn6dYrPmrS08rAXtQQ==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/dom-ready@3.27.0': resolution: {integrity: sha512-X7yVAm/JL5UKNfttAN2Ak3suEyOag/MPfr/aX8L2k/od71a6zJBkpMcdKaVPVfIPj9HcrW6ROrfINySPtoGCLA==} engines: {node: '>=12'} @@ -11052,8 +11068,8 @@ packages: resolution: {integrity: sha512-G6OnfLLC0MIWi9efTW6DMNEtEoy7tCoV0MxD19gUuG3/rDOi8RgHYwuNCvt6ieQMISiLiHnsg4tZc4D45zdfZA==} engines: {node: '>=12'} - '@wordpress/dom-ready@4.6.0': - resolution: {integrity: sha512-3fX1O1abmp3++FpZMPnDQygeygUggqfEvWQQQ80di/ksMEo6DXvIdtXolwDQt9WIC1WetLdI7Mf3KKVJnruyxg==} + '@wordpress/dom-ready@4.7.0': + resolution: {integrity: sha512-moMbRCPNfgCc0dT0waEr0renEsptnDMV89fGpMijA66IyvYoYsxDT57w2JqHiaKbTvbIBmgdNgDjcVgZGv5JoA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} '@wordpress/dom@2.18.0': @@ -11075,6 +11091,10 @@ packages: resolution: {integrity: sha512-ZCjMOya5dTkzgp/vTq7w1qpvVQDPoF7sJpalARUUQjeMUkUw/PTLYvvXJ3gARBCgaEdD85QjLorpxnJVz1XNng==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/dom@4.7.0': + resolution: {integrity: sha512-lGPEJHSHOT5Y9gWsX8V2tcsd5shDCTJqDxzL+pwDTfEsi/Os52nZCvzmavzGwRDzlm2Wmd3wNW+k/UCS/PhmXQ==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/e2e-test-utils-playwright@1.0.1': resolution: {integrity: sha512-DNR45Q0px6p3XLnJzRXANIXSQ1OKLdWCwQLQctuSmhVyqSyKS0VZApiYVoaPTKLEdxl+WeJ7jN153q1vUa5Lcg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11184,6 +11204,10 @@ packages: resolution: {integrity: sha512-IvSocvmd0fNus/XZo7K1EU4UD7aOKUdi3Y7pFUW2ljBbL3vuXk3E+6bwYahCjUIlBhpgGuCjemWTdg2Awzfmiw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/element@6.7.0': + resolution: {integrity: sha512-d0kiN8DCNDNoh5P5xLb496amoadvjsSnkyJHmQsw17qP4dHZaSLONiMi9yh3NQlwIu0pcbbn3WI/9ENA79HlFQ==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/env@10.5.0': resolution: {integrity: sha512-Hx+fi6qTEAuycznulkuMi4d5RDPZ6lPPAxaylpCwXNX2hgx5jrrpgnY4Zn0chBgZMpShO7BbA+zNDq2E6evvTw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11204,6 +11228,10 @@ packages: resolution: {integrity: sha512-NY9As0uJ81TPTogBzD6G/m7L4+sjvkjTEKkNsHLD5aEYxRX+RHlPYPyyd6y4CmlOkttwymbV9eKNP+LrfX5zZQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/escape-html@3.7.0': + resolution: {integrity: sha512-VqLQGNMs1BF6LnS+5eNjpM/sCUQhjn4QOfhDlWdVDi0ZxpZgssPzKhJ1ils/7FC0qF3vrMg8EH5xXxw2xz8A/w==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/eslint-plugin@12.9.0': resolution: {integrity: sha512-R6dTvD4uFYeoUJFZNUhm1CSwthC0Pl0RIY057Y9oUvGSqjjm7RqRIwKrMlw3dO0P9KoBGGHUox8NUj6EciRXww==} engines: {node: '>=12', npm: '>=6.9'} @@ -11277,6 +11305,10 @@ packages: resolution: {integrity: sha512-FWJhubBXeyRhx12YUmxT9pNoV9Azvx8nkynhduV+RNgA+F2SXoOf15pr+USPV//m3Bx031GN/wPHjgUCbC6+XA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/hooks@4.7.0': + resolution: {integrity: sha512-EGHMsNCt+PyStm3o1JWujaTA+HKcTxuEXdSHBBFDavzsgOF13bxTf1LpDYgTZJT3K9TSMP983IwfckP5t66pDw==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/html-entities@3.24.0': resolution: {integrity: sha512-rwvx8aEJb9gRCj/pJ0v7vh6sT7R6G922LQzHc5cObcSm5cmzPz/Wz07+AZkHA1cmCDQdPiDd3yB8X8l+yeFy0A==} engines: {node: '>=12'} @@ -11293,8 +11325,8 @@ packages: resolution: {integrity: sha512-Nb0nCYIdTEehWJ6HoA76bxpseKDY/12rYZ10eqf5OSr6oMvtyJ5j4fkNMKuHFQ00Mhppl9fkYWp2c8ZzBcp5Vw==} engines: {node: '>=12'} - '@wordpress/html-entities@4.6.0': - resolution: {integrity: sha512-ypTlGwDKw7jpmu9rneErkkq9dFHXzju8SGdEWkVAeqhRS9Ifri9DvmrovASB2c5IPY+Ijwh4YlVkx1yNBRHr5w==} + '@wordpress/html-entities@4.7.0': + resolution: {integrity: sha512-dVhbaGyQaDFwMoZn5PT+d6amO3VYurVQN/bkUl6h6SeBNOsTY1DqUVzO0rLxFp8Is/4MOms61sFJL7nvWtkxaA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} '@wordpress/i18n@3.20.0': @@ -11331,10 +11363,19 @@ packages: engines: {node: '>=18.12.0', npm: '>=8.19.2'} hasBin: true + '@wordpress/i18n@5.7.0': + resolution: {integrity: sha512-o1cq1zutE5rMAM//Ra1hRfgHuWNBxFtd7XNk+BuAcILRENMaEoqAoGBmGj9lRtOcqAj+cdoWxFjBIxRa67vIrg==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + hasBin: true + '@wordpress/icons@10.6.0': resolution: {integrity: sha512-dy58bQFVee2izXA65Ptar1f8mVhL1hilOJI3BWbLWmxHr9H4VjI0ohjW4ZkAhahBG2yIvKZja/HaFMTs5O/7Xg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/icons@10.7.0': + resolution: {integrity: sha512-4cvi9ZIaz6IYRcOjVuALtDLPtzgt1zK+E9LskL0PAi3TJhoh746q28wv6ycP+KtJEiI+bsTf2Qu5dmCePGR/jA==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/icons@4.1.0': resolution: {integrity: sha512-1FpEjT9kJbr0cWbgdgIwd2DoeerWijcVx3qCZ/WMFKNElBH9lfZLuWPI1hpX102HGWFcEi3VlbVpdBGeCeYQWg==} engines: {node: '>=12'} @@ -11396,6 +11437,10 @@ packages: resolution: {integrity: sha512-WjxXleJePz9scpTXMTl//mn3AgEBqdHd56pWtaDgz9Ub7O5H8AMNa2BU4VDK8OOQ3iwpAUgqGhaTRK5GjbaeSA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/is-shallow-equal@5.7.0': + resolution: {integrity: sha512-PW+OEkojwd8pZs7m8m9jVwVhLTA1kxmf01f0R2aC+bGfYvw0mlqcviCQTR6+EpRYpceh2nkDch2mD/LWT8c7ZA==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/jest-console@3.10.0': resolution: {integrity: sha512-iS1GSO+o7+p2PhvScOquD+IK7WqmVxa2s9uTUQyNEo06f9EUv6KNw0B1iZ00DpbgLqDCiczfdCNapC816UXIIA==} engines: {node: '>=8'} @@ -11499,6 +11544,10 @@ packages: resolution: {integrity: sha512-7jmKM1BLyoQPLXFl+3FPaKBrLEe7kUIkBMGS88083SQtXXFcW8sYQt5jd6E1yY6EAnniGveUNrv0C9Lbaipx3w==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/keycodes@4.7.0': + resolution: {integrity: sha512-x8I0xjRM8U0RnpFHWN9mA+x3MqjhJNBldiCpb59GTi3BIzPeDPgxbosAsAAgF0pYdDtGyiRkrOZA23NTia63TA==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/lazy-import@1.34.0': resolution: {integrity: sha512-ZF4YhWDJtvlev1GqZ7FRr2CPg5Vssw6lb4gn2OH56/KWuHf/LrBPVdshXR6ujDPvgUMnNFRf39ofHIENoj7JPA==} engines: {npm: '>=6.9.0'} @@ -11656,6 +11705,12 @@ packages: peerDependencies: react: ^18.0.0 + '@wordpress/primitives@4.7.0': + resolution: {integrity: sha512-PcAAIMT8+WqKB2HAeQlLmrcQyzyhNw9IeToJoxz+VKcc/7uLfGHplsDvtHY/X4jH8QlwlVwHSiqW/McTcxoUvQ==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + '@wordpress/priority-queue@1.11.2': resolution: {integrity: sha512-ulwmUOklY3orn1xXpcPnTyGWV5B/oycxI+cHZ6EevBVgM5sq+BW3xo0PKLR/MMm6UNBtFTu/71QAJrNZcD6V1g==} @@ -11671,6 +11726,10 @@ packages: resolution: {integrity: sha512-r2cyisWaqDLesIqC8BqWoXyNIxt1lwjvevw5Kijl9zxzxfYBsNQlu7RI1JNYgnjbDQQirWukFgprt7tdzhwssQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/priority-queue@3.7.0': + resolution: {integrity: sha512-WgKOhaQdaEeOxRLL49cp2YKfsZyUsR1qHoLid64Jux9FjFqLT8t52UTYJ796AhU4W0ifxf3R1SkNpW5zslxKOg==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/private-apis@0.20.0': resolution: {integrity: sha512-byyPRUNAD8/ca9N8gP2rUr8DHuMbzSoXO03nP8g3cecTN6iCOWqFDm6adkrbiAX527N9Ip+GOrRJd7Tta4kRIg==} engines: {node: '>=12'} @@ -11695,6 +11754,10 @@ packages: resolution: {integrity: sha512-gQ978Fh3QbwzcWLkljYYyqMIbj1zNK/4tjt6zwzjzCxwwh2qcChDVfVt358wjKJ5sGdhYKIbqP8NzQQV9Un0Iw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/private-apis@1.7.0': + resolution: {integrity: sha512-H6bbWZRL7u2awmK14ZCz7OupeIjz1HxSlB785X53k9JZ5KsbSK/FCzAvOJ5vCU9poC1fa6IT33qkgx3JNX3JEA==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/react-i18n@3.55.0': resolution: {integrity: sha512-1mbLk3MeK4qmY0yaqozXfNap9H6pKbTY5u5ZI3rdO7ZHYkrDH+mH0BnmkmkMDFGg3la8z7xQ82EvkPewXEUJlA==} engines: {node: '>=12'} @@ -11720,6 +11783,12 @@ packages: peerDependencies: redux: '>=4' + '@wordpress/redux-routine@5.7.0': + resolution: {integrity: sha512-KOU1+qFEDrptLY6lOQ3pTR+MZwe35dHfCp8xJHUJa/RI9jKULvXWrEIX0qhEMNWlinyQmwdTfmZqKxP8RuFzag==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + redux: '>=4' + '@wordpress/reusable-blocks@3.20.0': resolution: {integrity: sha512-2Wp1W704eYfTdCrYx+EKr5VbW/Z0AX24M8+FxWmhFlGjWpdzGl9shuMKv6cLfXeLDitU8fyHILXAVAXsvRvK3A==} engines: {node: '>=12'} @@ -11756,8 +11825,8 @@ packages: peerDependencies: react: ^18.0.0 - '@wordpress/rich-text@7.6.0': - resolution: {integrity: sha512-XxlfrlwfCPX7f3u9DMinouYNM9PDBMeGZb4MlK2Fbrc8ympaTZOdH4U74VR3jgv0Eusx6vxFEA5JVVXpW/xS2w==} + '@wordpress/rich-text@7.7.0': + resolution: {integrity: sha512-is96sOolYVeE/58jUr6GxZKY1XGWrF988lT8FUg7U4u0KgdDSIEPLacs4USE8OoqxZYCIAnwSPenMXN+ZPvOfw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} peerDependencies: react: ^18.0.0 @@ -11873,6 +11942,10 @@ packages: resolution: {integrity: sha512-Sl2rG/7t5zTQOgp+jOPn5m27sKd1DJIX/EGhM6LtRcjXZqa0rLDJXal1xWfkZk5oghaqW1TAwXJsg9UdAlh7Nw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/undo-manager@1.7.0': + resolution: {integrity: sha512-FHkwMD/jbe5jhVXfD9bSNhEivhMeszm20/ymEP6vAsLVJB2K25iAMOGvsq5jtznyJiqQzNUmvPERN0IKnaHQnA==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/url@2.22.2': resolution: {integrity: sha512-aqpYKQXzyzkCOm+GzZRYlLb+wh58g0cwR1PaKAl0UXaBS4mdS+X6biMriylb4P8CVC/RR7CSw5XI20JC24KDwQ==} @@ -11925,8 +11998,8 @@ packages: resolution: {integrity: sha512-Xs37x0IkvNewPNKs1A8cnw5xLb+AqwUqqCsH4+5Sjat5GDqP86mHgLfRIlE4d6fBYg+q6tO7DVPG49TT3/wzgA==} engines: {node: '>=12'} - '@wordpress/warning@3.6.0': - resolution: {integrity: sha512-pm57z1LZkzfQsXsji6yxcP0XSymKbvP087vJLlMkmLf+MoNVyTD6UvFpXl8hRSH6C6pySoJSgGFXaH81CRuO2Q==} + '@wordpress/warning@3.7.0': + resolution: {integrity: sha512-wGbQfPlf8YV6gGhcGPYWUhHORct4xaBQSaDTJrwzlgHYyrrJUVXXgZxaM4+Aa23zQoA13nvFQHvfssOkwdh65g==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} '@wordpress/widgets@3.24.0': @@ -21629,7 +21702,7 @@ packages: engines: {node: '>=18'} hasBin: true peerDependencies: - react: 18.2.0 + react: ^17.0.2 react-number-format@4.9.3: resolution: {integrity: sha512-am1A1xYAbENuKJ+zpM7V+B1oRTSeOHYltqVKExznIVFweBzhLmOBmyb1DfIKjHo90E0bo1p3nzVJ2NgS5xh+sQ==} @@ -21819,7 +21892,7 @@ packages: react-with-direction@1.4.0: resolution: {integrity: sha512-ybHNPiAmaJpoWwugwqry9Hd1Irl2hnNXlo/2SXQBwbLn/jGMauMS2y9jw+ydyX5V9ICryCqObNSthNt5R94xpg==} peerDependencies: - react: ^0.14 || ^15 || ^16 + react: ^17.0.2 react-dom: ^0.14 || ^15 || ^16 react-with-styles-interface-css@4.0.3: @@ -25603,7 +25676,7 @@ snapshots: '@wordpress/primitives': 3.55.0 '@wordpress/react-i18n': 3.55.0 classnames: 2.3.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) react: 17.0.2 react-dom: 17.0.2(react@17.0.2) react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -25628,7 +25701,7 @@ snapshots: '@wordpress/primitives': 3.55.0 '@wordpress/react-i18n': 3.55.0 classnames: 2.3.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) react: 17.0.2 react-dom: 17.0.2(react@17.0.2) react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -25732,7 +25805,7 @@ snapshots: '@babel/traverse': 7.23.5 '@babel/types': 7.23.5 convert-source-map: 1.9.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) gensync: 1.0.0-beta.2 json5: 2.2.3 lodash: 4.17.21 @@ -25755,7 +25828,7 @@ snapshots: '@babel/traverse': 7.23.5 '@babel/types': 7.23.5 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -25775,7 +25848,7 @@ snapshots: '@babel/traverse': 7.23.5 '@babel/types': 7.23.5 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -25838,14 +25911,6 @@ snapshots: eslint-visitor-keys: 2.1.0 semver: 6.3.1 - '@babel/eslint-parser@7.23.3(@babel/core@7.24.7)(eslint@7.32.0)': - dependencies: - '@babel/core': 7.24.7 - '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 - eslint: 7.32.0 - eslint-visitor-keys: 2.1.0 - semver: 6.3.1 - '@babel/eslint-parser@7.23.3(@babel/core@7.25.2)(eslint@7.32.0)': dependencies: '@babel/core': 7.25.2 @@ -26010,19 +26075,6 @@ snapshots: '@babel/helper-split-export-declaration': 7.24.7 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.23.6(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-function-name': 7.24.7 - '@babel/helper-member-expression-to-functions': 7.23.0 - '@babel/helper-optimise-call-expression': 7.22.5 - '@babel/helper-replace-supers': 7.22.20(@babel/core@7.24.7) - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/helper-split-export-declaration': 7.24.7 - semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.23.6(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -27014,6 +27066,7 @@ snapshots: dependencies: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.8 + optional: true '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.25.2)': dependencies: @@ -27250,11 +27303,6 @@ snapshots: '@babel/core': 7.23.2 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-jsx@7.22.5(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-jsx@7.22.5(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -28866,11 +28914,6 @@ snapshots: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-transform-react-display-name@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-transform-react-display-name@7.24.7(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -28890,13 +28933,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx-development@7.22.5(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/plugin-transform-react-jsx': 7.25.2(@babel/core@7.24.7) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-react-jsx-development@7.22.5(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -28988,17 +29024,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-module-imports': 7.24.7 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.24.7) - '@babel/types': 7.25.2 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -29022,12 +29047,6 @@ snapshots: '@babel/helper-annotate-as-pure': 7.24.7 '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-transform-react-pure-annotations@7.23.3(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-transform-react-pure-annotations@7.23.3(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -29340,13 +29359,13 @@ snapshots: '@babel/helper-plugin-utils': 7.24.8 '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.2) - '@babel/plugin-transform-typescript@7.23.6(@babel/core@7.24.7)': + '@babel/plugin-transform-typescript@7.23.6(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.7 + '@babel/core': 7.25.2 '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-create-class-features-plugin': 7.23.6(@babel/core@7.24.7) + '@babel/helper-create-class-features-plugin': 7.23.6(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.24.7) + '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.25.2) '@babel/plugin-transform-typescript@7.25.2(@babel/core@7.12.9)': dependencies: @@ -30173,18 +30192,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/preset-react@7.23.3(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/helper-validator-option': 7.24.8 - '@babel/plugin-transform-react-display-name': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-react-jsx': 7.25.2(@babel/core@7.24.7) - '@babel/plugin-transform-react-jsx-development': 7.22.5(@babel/core@7.24.7) - '@babel/plugin-transform-react-pure-annotations': 7.23.3(@babel/core@7.24.7) - transitivePeerDependencies: - - supports-color - '@babel/preset-react@7.23.3(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -30208,14 +30215,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.23.2(@babel/core@7.24.7)': + '@babel/preset-typescript@7.23.2(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.7 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.24.7) - '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.24.7) - '@babel/plugin-transform-typescript': 7.23.6(@babel/core@7.24.7) + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-transform-typescript': 7.23.6(@babel/core@7.25.2) transitivePeerDependencies: - supports-color @@ -30569,7 +30576,7 @@ snapshots: '@emotion/react@11.11.1(@types/react@17.0.71)(react@18.3.1)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@emotion/babel-plugin': 11.11.0 '@emotion/cache': 11.11.0 '@emotion/serialize': 1.1.2 @@ -30583,6 +30590,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@emotion/react@11.11.1(react@17.0.2)': + dependencies: + '@babel/runtime': 7.25.0 + '@emotion/babel-plugin': 11.11.0 + '@emotion/cache': 11.11.0 + '@emotion/serialize': 1.1.2 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@17.0.2) + '@emotion/utils': 1.2.1 + '@emotion/weak-memoize': 0.3.1 + hoist-non-react-statics: 3.3.2 + react: 17.0.2 + transitivePeerDependencies: + - supports-color + '@emotion/serialize@1.1.2': dependencies: '@emotion/hash': 0.9.1 @@ -30623,6 +30644,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@emotion/styled@11.11.0(@emotion/react@11.11.1(react@17.0.2))(react@17.0.2)': + dependencies: + '@babel/runtime': 7.25.0 + '@emotion/babel-plugin': 11.11.0 + '@emotion/is-prop-valid': 1.2.1 + '@emotion/react': 11.11.1(react@17.0.2) + '@emotion/serialize': 1.1.2 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@17.0.2) + '@emotion/utils': 1.2.1 + react: 17.0.2 + transitivePeerDependencies: + - supports-color + '@emotion/unitless@0.8.1': {} '@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@17.0.2)': @@ -32173,7 +32207,7 @@ snapshots: '@oclif/color': 1.0.13 '@oclif/core': 2.15.0(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3) chalk: 4.1.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) fs-extra: 9.1.0 http-call: 5.3.0 load-json-file: 5.3.0 @@ -32662,7 +32696,7 @@ snapshots: '@puppeteer/browsers@1.4.6(typescript@5.3.2)': dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.3.0 @@ -32676,7 +32710,7 @@ snapshots: '@puppeteer/browsers@1.4.6(typescript@5.3.3)': dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.3.0 @@ -32690,7 +32724,7 @@ snapshots: '@puppeteer/browsers@1.9.0': dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.3.1 @@ -38415,7 +38449,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.56.0 '@typescript-eslint/type-utils': 5.56.0(eslint@8.55.0)(typescript@5.3.2) '@typescript-eslint/utils': 5.56.0(eslint@8.55.0)(typescript@5.3.2) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) eslint: 8.55.0 grapheme-splitter: 1.0.4 ignore: 5.3.0 @@ -38434,7 +38468,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.55.0)(typescript@5.3.2) '@typescript-eslint/utils': 5.62.0(eslint@8.55.0)(typescript@5.3.2) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) eslint: 8.55.0 graphemer: 1.4.0 ignore: 5.3.0 @@ -38453,7 +38487,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.55.0)(typescript@5.3.3) '@typescript-eslint/utils': 5.62.0(eslint@8.55.0)(typescript@5.3.3) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) eslint: 8.55.0 graphemer: 1.4.0 ignore: 5.3.0 @@ -38533,7 +38567,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.56.0 '@typescript-eslint/types': 5.56.0 '@typescript-eslint/typescript-estree': 5.56.0(typescript@5.3.2) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) eslint: 8.55.0 optionalDependencies: typescript: 5.3.2 @@ -39084,7 +39118,7 @@ snapshots: webpack: 5.91.0(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@4.10.0) webpack-cli: 4.10.0(webpack-bundle-analyzer@4.7.0)(webpack-dev-server@4.15.1)(webpack@5.91.0) - '@webpack-cli/configtest@1.2.0(webpack-cli@4.10.0(webpack@5.89.0))(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0))': + '@webpack-cli/configtest@1.2.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0))(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0))': dependencies: webpack: 5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0) webpack-cli: 4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0) @@ -39104,7 +39138,7 @@ snapshots: envinfo: 7.13.0 webpack-cli: 4.10.0(webpack-bundle-analyzer@4.7.0)(webpack-dev-server@4.15.1)(webpack@5.91.0) - '@webpack-cli/info@1.5.0(webpack-cli@4.10.0(webpack@5.89.0))': + '@webpack-cli/info@1.5.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0))': dependencies: envinfo: 7.13.0 webpack-cli: 4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0) @@ -39262,11 +39296,11 @@ snapshots: '@wordpress/dom-ready': 3.27.0 '@wordpress/i18n': 4.6.1 - '@wordpress/a11y@4.6.0': + '@wordpress/a11y@4.7.0': dependencies: '@babel/runtime': 7.25.0 - '@wordpress/dom-ready': 4.6.0 - '@wordpress/i18n': 5.6.0 + '@wordpress/dom-ready': 4.7.0 + '@wordpress/i18n': 5.7.0 '@wordpress/api-fetch@3.23.1(react-native@0.73.0(@babel/core@7.23.5)(@babel/preset-env@7.23.6(@babel/core@7.23.5))(encoding@0.1.13)(react@17.0.2))': dependencies: @@ -39462,7 +39496,7 @@ snapshots: '@wordpress/element': 4.20.0 '@wordpress/hooks': 3.57.0 '@wordpress/html-entities': 3.24.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/i18n': 4.57.0 '@wordpress/icons': 9.36.0 '@wordpress/is-shallow-equal': 4.24.0 '@wordpress/keyboard-shortcuts': 3.20.0(react@17.0.2) @@ -39473,7 +39507,7 @@ snapshots: '@wordpress/style-engine': 1.30.0 '@wordpress/token-list': 2.47.0 '@wordpress/url': 3.48.0 - '@wordpress/warning': 2.47.0 + '@wordpress/warning': 2.57.0 '@wordpress/wordcount': 3.47.0 change-case: 4.1.2 classnames: 2.3.2 @@ -39678,7 +39712,7 @@ snapshots: '@wordpress/element': 4.20.0 '@wordpress/hooks': 3.57.0 '@wordpress/html-entities': 3.24.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/i18n': 4.57.0 '@wordpress/icons': 8.4.0 '@wordpress/is-shallow-equal': 4.24.0 '@wordpress/keyboard-shortcuts': 3.20.0(react@17.0.2) @@ -39689,7 +39723,7 @@ snapshots: '@wordpress/style-engine': 0.6.0 '@wordpress/token-list': 2.47.0 '@wordpress/url': 3.13.0 - '@wordpress/warning': 2.47.0 + '@wordpress/warning': 2.57.0 '@wordpress/wordcount': 3.47.0 classnames: 2.3.2 colord: 2.9.3 @@ -39889,7 +39923,7 @@ snapshots: '@wordpress/blocks@11.21.0(react@17.0.2)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@wordpress/autop': 3.47.0 '@wordpress/blob': 3.47.0 '@wordpress/block-serialization-default-parser': 4.47.0 @@ -39900,7 +39934,7 @@ snapshots: '@wordpress/element': 4.20.0 '@wordpress/hooks': 3.57.0 '@wordpress/html-entities': 3.24.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/i18n': 4.57.0 '@wordpress/is-shallow-equal': 4.24.0 '@wordpress/shortcode': 3.47.0 change-case: 4.1.2 @@ -39918,7 +39952,7 @@ snapshots: '@wordpress/blocks@11.21.0(react@18.3.1)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@wordpress/autop': 3.47.0 '@wordpress/blob': 3.47.0 '@wordpress/block-serialization-default-parser': 4.47.0 @@ -39929,7 +39963,7 @@ snapshots: '@wordpress/element': 4.20.0 '@wordpress/hooks': 3.57.0 '@wordpress/html-entities': 3.24.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/i18n': 4.57.0 '@wordpress/is-shallow-equal': 4.24.0 '@wordpress/shortcode': 3.47.0 change-case: 4.1.2 @@ -40230,6 +40264,55 @@ snapshots: - react-with-direction - supports-color + '@wordpress/components@19.17.0(react-dom@17.0.2(react@17.0.2))(react-with-direction@1.4.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2)': + dependencies: + '@babel/runtime': 7.23.5 + '@emotion/cache': 11.11.0 + '@emotion/css': 11.11.2 + '@emotion/react': 11.11.1(react@17.0.2) + '@emotion/serialize': 1.1.2 + '@emotion/styled': 11.11.0(@emotion/react@11.11.1(react@17.0.2))(react@17.0.2) + '@emotion/utils': 1.0.0 + '@floating-ui/react-dom': 0.6.3(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@use-gesture/react': 10.3.0(react@17.0.2) + '@wordpress/a11y': 3.47.0 + '@wordpress/compose': 5.20.0(react@17.0.2) + '@wordpress/date': 4.44.0 + '@wordpress/deprecated': 3.41.0 + '@wordpress/dom': 3.27.0 + '@wordpress/element': 4.20.0 + '@wordpress/escape-html': 2.47.0 + '@wordpress/hooks': 3.57.0 + '@wordpress/i18n': 4.47.0 + '@wordpress/icons': 9.36.0 + '@wordpress/is-shallow-equal': 4.24.0 + '@wordpress/keycodes': 3.47.0 + '@wordpress/primitives': 3.45.0 + '@wordpress/rich-text': 5.20.0(react@17.0.2) + '@wordpress/warning': 2.47.0 + classnames: 2.3.2 + colord: 2.9.3 + dom-scroll-into-view: 1.2.1 + downshift: 6.1.12(react@17.0.2) + framer-motion: 6.5.1(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + gradient-parser: 0.1.5 + highlight-words-core: 1.2.2 + lodash: 4.17.21 + memize: 1.1.0 + moment: 2.29.4 + re-resizable: 6.9.11(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + react: 17.0.2 + react-colorful: 5.6.1(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + react-dates: 21.8.0(@babel/runtime@7.23.5)(moment@2.29.4)(react-dom@17.0.2(react@17.0.2))(react-with-direction@1.4.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2) + react-dom: 17.0.2(react@17.0.2) + reakit: 1.3.11(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + remove-accents: 0.4.4 + uuid: 8.3.2 + transitivePeerDependencies: + - '@types/react' + - react-with-direction + - supports-color + '@wordpress/components@19.8.5(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react-with-direction@1.4.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2)': dependencies: '@babel/runtime': 7.23.5 @@ -40326,6 +40409,102 @@ snapshots: - react-with-direction - supports-color + '@wordpress/components@19.8.5(react-dom@17.0.2(react@17.0.2))(react-with-direction@1.4.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2)': + dependencies: + '@babel/runtime': 7.23.5 + '@emotion/cache': 11.11.0 + '@emotion/css': 11.11.2 + '@emotion/react': 11.11.1(react@17.0.2) + '@emotion/serialize': 1.1.2 + '@emotion/styled': 11.11.0(@emotion/react@11.11.1(react@17.0.2))(react@17.0.2) + '@emotion/utils': 1.0.0 + '@use-gesture/react': 10.3.0(react@17.0.2) + '@wordpress/a11y': 3.6.1 + '@wordpress/compose': 5.4.1(react@17.0.2) + '@wordpress/date': 4.44.0 + '@wordpress/deprecated': 3.41.0 + '@wordpress/dom': 3.6.1 + '@wordpress/element': 4.4.1 + '@wordpress/escape-html': 2.47.0 + '@wordpress/hooks': 3.6.1 + '@wordpress/i18n': 4.6.1 + '@wordpress/icons': 8.4.0 + '@wordpress/is-shallow-equal': 4.24.0 + '@wordpress/keycodes': 3.47.0 + '@wordpress/primitives': 3.4.1 + '@wordpress/rich-text': 5.4.2(react@17.0.2) + '@wordpress/warning': 2.6.1 + classnames: 2.3.2 + colord: 2.9.3 + dom-scroll-into-view: 1.2.1 + downshift: 6.1.12(react@17.0.2) + framer-motion: 6.5.1(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + gradient-parser: 0.1.5 + highlight-words-core: 1.2.2 + lodash: 4.17.21 + memize: 1.1.0 + moment: 2.29.4 + re-resizable: 6.9.11(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + react: 17.0.2 + react-colorful: 5.6.1(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + react-dates: 17.2.0(moment@2.29.4)(react-dom@17.0.2(react@17.0.2))(react-with-direction@1.4.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2) + react-dom: 17.0.2(react@17.0.2) + react-resize-aware: 3.1.1(react@17.0.2) + reakit: 1.3.11(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + uuid: 8.3.2 + transitivePeerDependencies: + - '@types/react' + - react-with-direction + - supports-color + + '@wordpress/components@19.8.5(react-dom@18.3.1(react@17.0.2))(react-with-direction@1.4.0(react-dom@18.3.1(react@17.0.2))(react@17.0.2))(react@17.0.2)': + dependencies: + '@babel/runtime': 7.23.5 + '@emotion/cache': 11.11.0 + '@emotion/css': 11.11.2 + '@emotion/react': 11.11.1(react@17.0.2) + '@emotion/serialize': 1.1.2 + '@emotion/styled': 11.11.0(@emotion/react@11.11.1(react@17.0.2))(react@17.0.2) + '@emotion/utils': 1.0.0 + '@use-gesture/react': 10.3.0(react@17.0.2) + '@wordpress/a11y': 3.6.1 + '@wordpress/compose': 5.4.1(react@17.0.2) + '@wordpress/date': 4.44.0 + '@wordpress/deprecated': 3.41.0 + '@wordpress/dom': 3.6.1 + '@wordpress/element': 4.4.1 + '@wordpress/escape-html': 2.47.0 + '@wordpress/hooks': 3.6.1 + '@wordpress/i18n': 4.6.1 + '@wordpress/icons': 8.4.0 + '@wordpress/is-shallow-equal': 4.24.0 + '@wordpress/keycodes': 3.47.0 + '@wordpress/primitives': 3.4.1 + '@wordpress/rich-text': 5.4.2(react@17.0.2) + '@wordpress/warning': 2.6.1 + classnames: 2.3.2 + colord: 2.9.3 + dom-scroll-into-view: 1.2.1 + downshift: 6.1.12(react@17.0.2) + framer-motion: 6.5.1(react-dom@18.3.1(react@17.0.2))(react@17.0.2) + gradient-parser: 0.1.5 + highlight-words-core: 1.2.2 + lodash: 4.17.21 + memize: 1.1.0 + moment: 2.29.4 + re-resizable: 6.9.11(react-dom@18.3.1(react@17.0.2))(react@17.0.2) + react: 17.0.2 + react-colorful: 5.6.1(react-dom@18.3.1(react@17.0.2))(react@17.0.2) + react-dates: 17.2.0(moment@2.29.4)(react-dom@18.3.1(react@17.0.2))(react-with-direction@1.4.0(react-dom@18.3.1(react@17.0.2))(react@17.0.2))(react@17.0.2) + react-dom: 18.3.1(react@17.0.2) + react-resize-aware: 3.1.1(react@17.0.2) + reakit: 1.3.11(react-dom@18.3.1(react@17.0.2))(react@17.0.2) + uuid: 8.3.2 + transitivePeerDependencies: + - '@types/react' + - react-with-direction + - supports-color + '@wordpress/components@20.0.0(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: '@babel/runtime': 7.25.0 @@ -40345,13 +40524,13 @@ snapshots: '@wordpress/element': 4.20.0 '@wordpress/escape-html': 2.47.0 '@wordpress/hooks': 3.57.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/i18n': 4.57.0 '@wordpress/icons': 9.36.0 '@wordpress/is-shallow-equal': 4.24.0 '@wordpress/keycodes': 3.47.0 '@wordpress/primitives': 3.55.0 '@wordpress/rich-text': 5.20.0(react@17.0.2) - '@wordpress/warning': 2.47.0 + '@wordpress/warning': 2.57.0 change-case: 4.1.2 classnames: 2.3.2 colord: 2.9.3 @@ -40392,15 +40571,15 @@ snapshots: '@wordpress/deprecated': 3.41.0 '@wordpress/dom': 3.27.0 '@wordpress/element': 4.20.0 - '@wordpress/escape-html': 2.47.0 + '@wordpress/escape-html': 2.57.0 '@wordpress/hooks': 3.57.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/i18n': 4.57.0 '@wordpress/icons': 9.36.0 '@wordpress/is-shallow-equal': 4.24.0 - '@wordpress/keycodes': 3.47.0 + '@wordpress/keycodes': 3.57.0 '@wordpress/primitives': 3.55.0 '@wordpress/rich-text': 5.20.0(react@17.0.2) - '@wordpress/warning': 2.47.0 + '@wordpress/warning': 2.57.0 change-case: 4.1.2 classnames: 2.3.2 colord: 2.9.3 @@ -40668,7 +40847,7 @@ snapshots: - '@types/react' - supports-color - '@wordpress/components@28.6.0(@emotion/is-prop-valid@1.2.1)(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + '@wordpress/components@28.7.0(@emotion/is-prop-valid@1.2.1)(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: '@ariakit/react': 0.4.10(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@babel/runtime': 7.25.0 @@ -40682,23 +40861,23 @@ snapshots: '@types/gradient-parser': 0.1.3 '@types/highlight-words-core': 1.2.1 '@use-gesture/react': 10.3.1(react@17.0.2) - '@wordpress/a11y': 4.6.0 - '@wordpress/compose': 7.6.0(react@17.0.2) - '@wordpress/date': 5.6.0 - '@wordpress/deprecated': 4.6.0 - '@wordpress/dom': 4.6.0 - '@wordpress/element': 6.6.0 - '@wordpress/escape-html': 3.6.0 - '@wordpress/hooks': 4.6.0 - '@wordpress/html-entities': 4.6.0 - '@wordpress/i18n': 5.6.0 - '@wordpress/icons': 10.6.0(react@17.0.2) - '@wordpress/is-shallow-equal': 5.6.0 - '@wordpress/keycodes': 4.6.0 - '@wordpress/primitives': 4.6.0(react@17.0.2) - '@wordpress/private-apis': 1.6.0 - '@wordpress/rich-text': 7.6.0(react@17.0.2) - '@wordpress/warning': 3.6.0 + '@wordpress/a11y': 4.7.0 + '@wordpress/compose': 7.7.0(react@17.0.2) + '@wordpress/date': 5.7.0 + '@wordpress/deprecated': 4.7.0 + '@wordpress/dom': 4.7.0 + '@wordpress/element': 6.7.0 + '@wordpress/escape-html': 3.7.0 + '@wordpress/hooks': 4.7.0 + '@wordpress/html-entities': 4.7.0 + '@wordpress/i18n': 5.7.0 + '@wordpress/icons': 10.7.0(react@17.0.2) + '@wordpress/is-shallow-equal': 5.7.0 + '@wordpress/keycodes': 4.7.0 + '@wordpress/primitives': 4.7.0(react@17.0.2) + '@wordpress/private-apis': 1.7.0 + '@wordpress/rich-text': 7.7.0(react@17.0.2) + '@wordpress/warning': 3.7.0 change-case: 4.1.2 clsx: 2.1.1 colord: 2.9.3 @@ -40931,6 +41110,23 @@ snapshots: react: 17.0.2 use-memo-one: 1.1.3(react@17.0.2) + '@wordpress/compose@7.7.0(react@17.0.2)': + dependencies: + '@babel/runtime': 7.25.0 + '@types/mousetrap': 1.6.15 + '@wordpress/deprecated': 4.7.0 + '@wordpress/dom': 4.7.0 + '@wordpress/element': 6.7.0 + '@wordpress/is-shallow-equal': 5.7.0 + '@wordpress/keycodes': 4.7.0 + '@wordpress/priority-queue': 3.7.0 + '@wordpress/undo-manager': 1.7.0 + change-case: 4.1.2 + clipboard: 2.0.11 + mousetrap: 1.6.5 + react: 17.0.2 + use-memo-one: 1.1.3(react@17.0.2) + '@wordpress/core-commands@0.7.0(@babel/helper-module-imports@7.24.7)(@babel/types@7.25.2)(@types/react@17.0.71)(babel-plugin-macros@3.1.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: '@babel/runtime': 7.25.0 @@ -40938,7 +41134,7 @@ snapshots: '@wordpress/core-data': 6.24.0(@babel/helper-module-imports@7.24.7)(@babel/types@7.25.2)(@types/react@17.0.71)(babel-plugin-macros@3.1.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@wordpress/data': 9.27.0(react@17.0.2) '@wordpress/element': 5.22.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/i18n': 4.57.0 '@wordpress/icons': 9.36.0 '@wordpress/private-apis': 0.20.0 '@wordpress/router': 0.7.0(react@17.0.2) @@ -40977,7 +41173,7 @@ snapshots: '@wordpress/core-data@5.5.0(react@17.0.2)': dependencies: - '@babel/runtime': 7.23.5 + '@babel/runtime': 7.25.0 '@wordpress/api-fetch': 6.21.0 '@wordpress/blocks': 11.21.0(react@17.0.2) '@wordpress/compose': 5.20.0(react@17.0.2) @@ -40985,7 +41181,7 @@ snapshots: '@wordpress/deprecated': 3.41.0 '@wordpress/element': 4.20.0 '@wordpress/html-entities': 3.24.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/i18n': 4.57.0 '@wordpress/is-shallow-equal': 4.24.0 '@wordpress/url': 3.48.0 change-case: 4.1.2 @@ -41158,6 +41354,25 @@ snapshots: rememo: 4.0.2 use-memo-one: 1.1.3(react@17.0.2) + '@wordpress/data@10.7.0(react@17.0.2)': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/compose': 7.7.0(react@17.0.2) + '@wordpress/deprecated': 4.7.0 + '@wordpress/element': 6.7.0 + '@wordpress/is-shallow-equal': 5.7.0 + '@wordpress/priority-queue': 3.7.0 + '@wordpress/private-apis': 1.7.0 + '@wordpress/redux-routine': 5.7.0(redux@4.2.1) + deepmerge: 4.3.1 + equivalent-key-map: 0.2.2 + is-plain-object: 5.0.0 + is-promise: 4.0.0 + react: 17.0.2 + redux: 4.2.1 + rememo: 4.0.2 + use-memo-one: 1.1.3(react@17.0.2) + '@wordpress/data@4.27.3(react@17.0.2)': dependencies: '@babel/runtime': 7.25.0 @@ -41378,19 +41593,19 @@ snapshots: rememo: 4.0.2 use-memo-one: 1.1.3(react@18.3.1) - '@wordpress/dataviews@4.2.0(@emotion/is-prop-valid@1.2.1)(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + '@wordpress/dataviews@4.3.0(@emotion/is-prop-valid@1.2.1)(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: '@ariakit/react': 0.4.10(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@babel/runtime': 7.25.0 - '@wordpress/components': 28.6.0(@emotion/is-prop-valid@1.2.1)(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@wordpress/compose': 7.6.0(react@17.0.2) - '@wordpress/data': 10.6.0(react@17.0.2) - '@wordpress/element': 6.6.0 - '@wordpress/i18n': 5.6.0 - '@wordpress/icons': 10.6.0(react@17.0.2) - '@wordpress/primitives': 4.6.0(react@17.0.2) - '@wordpress/private-apis': 1.6.0 - '@wordpress/warning': 3.6.0 + '@wordpress/components': 28.7.0(@emotion/is-prop-valid@1.2.1)(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@wordpress/compose': 7.7.0(react@17.0.2) + '@wordpress/data': 10.7.0(react@17.0.2) + '@wordpress/element': 6.7.0 + '@wordpress/i18n': 5.7.0 + '@wordpress/icons': 10.7.0(react@17.0.2) + '@wordpress/primitives': 4.7.0(react@17.0.2) + '@wordpress/private-apis': 1.7.0 + '@wordpress/warning': 3.7.0 clsx: 2.1.1 react: 17.0.2 remove-accents: 0.5.0 @@ -41420,10 +41635,10 @@ snapshots: moment: 2.29.4 moment-timezone: 0.5.43 - '@wordpress/date@5.6.0': + '@wordpress/date@5.7.0': dependencies: '@babel/runtime': 7.25.0 - '@wordpress/deprecated': 4.6.0 + '@wordpress/deprecated': 4.7.0 moment: 2.29.4 moment-timezone: 0.5.43 @@ -41493,6 +41708,11 @@ snapshots: '@babel/runtime': 7.25.0 '@wordpress/hooks': 4.6.0 + '@wordpress/deprecated@4.7.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/hooks': 4.7.0 + '@wordpress/dom-ready@3.27.0': dependencies: '@babel/runtime': 7.23.5 @@ -41509,7 +41729,7 @@ snapshots: dependencies: '@babel/runtime': 7.23.5 - '@wordpress/dom-ready@4.6.0': + '@wordpress/dom-ready@4.7.0': dependencies: '@babel/runtime': 7.25.0 @@ -41538,6 +41758,11 @@ snapshots: '@babel/runtime': 7.25.0 '@wordpress/deprecated': 4.6.0 + '@wordpress/dom@4.7.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/deprecated': 4.7.0 + '@wordpress/e2e-test-utils-playwright@1.0.1(@playwright/test@1.46.1)(encoding@0.1.13)(typescript@5.3.2)': dependencies: '@playwright/test': 1.46.1 @@ -42010,6 +42235,17 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@wordpress/element@6.7.0': + dependencies: + '@babel/runtime': 7.25.0 + '@types/react': 17.0.71 + '@types/react-dom': 18.3.0 + '@wordpress/escape-html': 3.7.0 + change-case: 4.1.2 + is-plain-object: 5.0.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@wordpress/env@10.5.0': dependencies: chalk: 4.1.2 @@ -42043,6 +42279,10 @@ snapshots: dependencies: '@babel/runtime': 7.25.0 + '@wordpress/escape-html@3.7.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/eslint-plugin@12.9.0(@babel/core@7.25.2)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.56.0(eslint@8.55.0)(typescript@5.3.2))(eslint-import-resolver-webpack@0.13.2)(eslint-plugin-import@2.28.1)(eslint@8.55.0))(eslint-import-resolver-webpack@0.13.2(eslint-plugin-import@2.28.1)(webpack@5.91.0(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@5.1.4)))(eslint@8.55.0)(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.2)))(typescript@5.3.2)(wp-prettier@2.6.2)': dependencies: '@babel/core': 7.25.2 @@ -42179,33 +42419,6 @@ snapshots: - supports-color - typescript - '@wordpress/eslint-plugin@9.3.0(@babel/core@7.24.7)(eslint@7.32.0)(typescript@5.3.3)': - dependencies: - '@babel/eslint-parser': 7.23.3(@babel/core@7.24.7)(eslint@7.32.0) - '@typescript-eslint/eslint-plugin': 4.33.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.3.3))(eslint@7.32.0)(typescript@5.3.3) - '@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@5.3.3) - '@wordpress/prettier-config': 1.4.0(wp-prettier@2.2.1-beta-1) - cosmiconfig: 7.1.0 - eslint: 7.32.0 - eslint-config-prettier: 7.2.0(eslint@7.32.0) - eslint-plugin-import: 2.29.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.3.3))(eslint@7.32.0) - eslint-plugin-jest: 24.7.0(@typescript-eslint/eslint-plugin@4.33.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.3.3))(eslint@7.32.0)(typescript@5.3.3))(eslint@7.32.0)(typescript@5.3.3) - eslint-plugin-jsdoc: 36.1.1(eslint@7.32.0) - eslint-plugin-jsx-a11y: 6.8.0(eslint@7.32.0) - eslint-plugin-prettier: 3.4.1(eslint-config-prettier@7.2.0(eslint@7.32.0))(eslint@7.32.0)(wp-prettier@2.2.1-beta-1) - eslint-plugin-react: 7.33.2(eslint@7.32.0) - eslint-plugin-react-hooks: 4.6.0(eslint@7.32.0) - globals: 12.4.0 - prettier: wp-prettier@2.2.1-beta-1 - requireindex: 1.2.0 - optionalDependencies: - typescript: 5.3.3 - transitivePeerDependencies: - - '@babel/core' - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - '@wordpress/eslint-plugin@9.3.0(@babel/core@7.25.2)(eslint@7.32.0)(typescript@5.3.3)': dependencies: '@babel/eslint-parser': 7.23.3(@babel/core@7.25.2)(eslint@7.32.0) @@ -42249,6 +42462,10 @@ snapshots: dependencies: '@babel/runtime': 7.25.0 + '@wordpress/hooks@4.7.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/html-entities@3.24.0': dependencies: '@babel/runtime': 7.23.5 @@ -42265,7 +42482,7 @@ snapshots: dependencies: '@babel/runtime': 7.23.5 - '@wordpress/html-entities@4.6.0': + '@wordpress/html-entities@4.7.0': dependencies: '@babel/runtime': 7.25.0 @@ -42334,6 +42551,15 @@ snapshots: sprintf-js: 1.1.3 tannin: 1.2.0 + '@wordpress/i18n@5.7.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/hooks': 4.7.0 + gettext-parser: 1.4.0 + memize: 2.1.0 + sprintf-js: 1.1.3 + tannin: 1.2.0 + '@wordpress/icons@10.6.0(react@17.0.2)': dependencies: '@babel/runtime': 7.25.0 @@ -42342,6 +42568,14 @@ snapshots: transitivePeerDependencies: - react + '@wordpress/icons@10.7.0(react@17.0.2)': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/element': 6.7.0 + '@wordpress/primitives': 4.7.0(react@17.0.2) + transitivePeerDependencies: + - react + '@wordpress/icons@4.1.0': dependencies: '@babel/runtime': 7.25.0 @@ -42456,6 +42690,10 @@ snapshots: dependencies: '@babel/runtime': 7.25.0 + '@wordpress/is-shallow-equal@5.7.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/jest-console@3.10.0(jest@25.5.4)': dependencies: '@babel/runtime': 7.25.0 @@ -42560,20 +42798,6 @@ snapshots: - react-dom - supports-color - '@wordpress/jest-preset-default@7.1.3(@babel/core@7.24.7)(jest@26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': - dependencies: - '@wojtekmaj/enzyme-adapter-react-17': 0.6.7(enzyme@3.11.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@wordpress/jest-console': 4.1.1(jest@26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))) - babel-jest: 26.6.3(@babel/core@7.24.7) - enzyme: 3.11.0 - enzyme-to-json: 3.6.2(enzyme@3.11.0) - jest: 26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)) - transitivePeerDependencies: - - '@babel/core' - - react - - react-dom - - supports-color - '@wordpress/jest-preset-default@7.1.3(@babel/core@7.25.2)(jest@26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: '@wojtekmaj/enzyme-adapter-react-17': 0.6.7(enzyme@3.11.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -42704,6 +42928,11 @@ snapshots: '@babel/runtime': 7.25.0 '@wordpress/i18n': 5.6.0 + '@wordpress/keycodes@4.7.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/i18n': 5.7.0 + '@wordpress/lazy-import@1.34.0': dependencies: execa: 4.1.0 @@ -42725,7 +42954,7 @@ snapshots: '@wordpress/api-fetch': 6.44.0 '@wordpress/blob': 3.47.0 '@wordpress/element': 5.34.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/i18n': 4.57.0 '@wordpress/notices@3.12.0(react@17.0.2)': dependencies: @@ -42745,7 +42974,7 @@ snapshots: dependencies: '@babel/runtime': 7.25.0 '@wordpress/a11y': 3.47.0 - '@wordpress/data': 9.17.0(react@17.0.2) + '@wordpress/data': 9.27.0(react@17.0.2) transitivePeerDependencies: - react @@ -43006,6 +43235,13 @@ snapshots: clsx: 2.1.1 react: 17.0.2 + '@wordpress/primitives@4.7.0(react@17.0.2)': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/element': 6.7.0 + clsx: 2.1.1 + react: 17.0.2 + '@wordpress/priority-queue@1.11.2': dependencies: '@babel/runtime': 7.25.0 @@ -43025,6 +43261,11 @@ snapshots: '@babel/runtime': 7.25.0 requestidlecallback: 0.3.0 + '@wordpress/priority-queue@3.7.0': + dependencies: + '@babel/runtime': 7.25.0 + requestidlecallback: 0.3.0 + '@wordpress/private-apis@0.20.0': dependencies: '@babel/runtime': 7.25.0 @@ -43049,6 +43290,10 @@ snapshots: dependencies: '@babel/runtime': 7.25.0 + '@wordpress/private-apis@1.7.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/react-i18n@3.55.0': dependencies: '@babel/runtime': 7.25.0 @@ -43087,6 +43332,14 @@ snapshots: redux: 4.2.1 rungen: 0.3.2 + '@wordpress/redux-routine@5.7.0(redux@4.2.1)': + dependencies: + '@babel/runtime': 7.25.0 + is-plain-object: 5.0.0 + is-promise: 4.0.0 + redux: 4.2.1 + rungen: 0.3.2 + '@wordpress/reusable-blocks@3.20.0(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: '@wordpress/block-editor': 10.5.0(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -43095,7 +43348,7 @@ snapshots: '@wordpress/core-data': 5.5.0(react@17.0.2) '@wordpress/data': 7.6.0(react@17.0.2) '@wordpress/element': 4.20.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/i18n': 4.57.0 '@wordpress/icons': 9.36.0 '@wordpress/notices': 3.31.0(react@17.0.2) '@wordpress/url': 3.48.0 @@ -43159,7 +43412,7 @@ snapshots: '@wordpress/deprecated': 3.41.0 '@wordpress/element': 4.20.0 '@wordpress/escape-html': 2.47.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/i18n': 4.57.0 '@wordpress/keycodes': 3.47.0 memize: 1.1.0 react: 17.0.2 @@ -43174,7 +43427,7 @@ snapshots: '@wordpress/deprecated': 3.41.0 '@wordpress/element': 4.20.0 '@wordpress/escape-html': 2.47.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/i18n': 4.57.0 '@wordpress/keycodes': 3.47.0 memize: 1.1.0 react: 18.3.1 @@ -43238,17 +43491,17 @@ snapshots: memize: 2.1.0 react: 18.3.1 - '@wordpress/rich-text@7.6.0(react@17.0.2)': + '@wordpress/rich-text@7.7.0(react@17.0.2)': dependencies: '@babel/runtime': 7.25.0 - '@wordpress/a11y': 4.6.0 - '@wordpress/compose': 7.6.0(react@17.0.2) - '@wordpress/data': 10.6.0(react@17.0.2) - '@wordpress/deprecated': 4.6.0 - '@wordpress/element': 6.6.0 - '@wordpress/escape-html': 3.6.0 - '@wordpress/i18n': 5.6.0 - '@wordpress/keycodes': 4.6.0 + '@wordpress/a11y': 4.7.0 + '@wordpress/compose': 7.7.0(react@17.0.2) + '@wordpress/data': 10.7.0(react@17.0.2) + '@wordpress/deprecated': 4.7.0 + '@wordpress/element': 6.7.0 + '@wordpress/escape-html': 3.7.0 + '@wordpress/i18n': 5.7.0 + '@wordpress/keycodes': 4.7.0 memize: 2.1.0 react: 17.0.2 @@ -43323,86 +43576,7 @@ snapshots: - utf-8-validate - webpack-command - '@wordpress/scripts@19.2.4(@babel/core@7.24.7)(debug@4.3.4)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4)': - dependencies: - '@svgr/webpack': 5.5.0 - '@wordpress/babel-preset-default': 6.17.0 - '@wordpress/browserslist-config': 4.1.3 - '@wordpress/dependency-extraction-webpack-plugin': 3.7.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) - '@wordpress/eslint-plugin': 9.3.0(@babel/core@7.24.7)(eslint@7.32.0)(typescript@5.3.3) - '@wordpress/jest-preset-default': 7.1.3(@babel/core@7.24.7)(jest@26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@wordpress/npm-package-json-lint-config': 4.32.0(npm-package-json-lint@5.4.2) - '@wordpress/postcss-plugins-preset': 3.6.1(postcss@8.4.32) - '@wordpress/prettier-config': 1.4.0(wp-prettier@2.2.1-beta-1) - '@wordpress/stylelint-config': 19.1.0(stylelint@13.13.1) - babel-jest: 26.6.3(@babel/core@7.24.7) - babel-loader: 8.3.0(@babel/core@7.24.7)(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) - browserslist: 4.19.3 - chalk: 4.1.2 - check-node-version: 4.2.1 - clean-webpack-plugin: 3.0.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) - cross-spawn: 5.1.0 - css-loader: 6.8.1(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) - cssnano: 5.1.12(postcss@8.4.32) - cwd: 0.10.0 - dir-glob: 3.0.1 - eslint: 7.32.0 - eslint-plugin-markdown: 2.2.1(eslint@7.32.0) - expect-puppeteer: 4.4.0 - filenamify: 4.3.0 - jest: 26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)) - jest-circus: 26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)) - jest-dev-server: 5.0.3(debug@4.3.4) - jest-environment-node: 26.6.2 - markdownlint: 0.23.1 - markdownlint-cli: 0.27.1 - merge-deep: 3.0.3 - mini-css-extract-plugin: 2.7.6(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) - minimist: 1.2.8 - npm-package-json-lint: 5.4.2 - postcss: 8.4.32 - postcss-loader: 6.2.1(postcss@8.4.32)(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) - prettier: wp-prettier@2.2.1-beta-1 - puppeteer-core: 10.4.0 - read-pkg-up: 1.0.1 - resolve-bin: 0.4.3 - sass: 1.69.5 - sass-loader: 12.6.0(sass@1.69.5)(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) - source-map-loader: 3.0.2(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) - stylelint: 13.13.1 - terser-webpack-plugin: 5.3.6(uglify-js@3.17.4)(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) - webpack: 5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0) - webpack-bundle-analyzer: 4.7.0 - webpack-cli: 4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0) - webpack-livereload-plugin: 3.0.2(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) - transitivePeerDependencies: - - '@babel/core' - - '@swc/core' - - '@webpack-cli/generators' - - '@webpack-cli/migrate' - - bufferutil - - canvas - - debug - - esbuild - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - fibers - - file-loader - - node-sass - - postcss-jsx - - postcss-markdown - - react - - react-dom - - sass-embedded - - supports-color - - ts-node - - typescript - - uglify-js - - utf-8-validate - - webpack-dev-server - - '@wordpress/scripts@19.2.4(@babel/core@7.25.2)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4)': + '@wordpress/scripts@19.2.4(@babel/core@7.25.2)(debug@4.3.4)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4)': dependencies: '@svgr/webpack': 5.5.0 '@wordpress/babel-preset-default': 6.17.0 @@ -43663,7 +43837,7 @@ snapshots: '@wordpress/data': 6.15.0(react@17.0.2) '@wordpress/deprecated': 3.41.0 '@wordpress/element': 4.20.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/i18n': 4.57.0 '@wordpress/url': 3.13.0 lodash: 4.17.21 react: 17.0.2 @@ -43703,7 +43877,7 @@ snapshots: '@wordpress/data': 7.6.0(react@17.0.2) '@wordpress/deprecated': 3.41.0 '@wordpress/element': 4.20.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/i18n': 4.57.0 '@wordpress/url': 3.48.0 lodash: 4.17.21 react: 17.0.2 @@ -43820,6 +43994,11 @@ snapshots: '@babel/runtime': 7.25.0 '@wordpress/is-shallow-equal': 5.6.0 + '@wordpress/undo-manager@1.7.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/is-shallow-equal': 5.7.0 + '@wordpress/url@2.22.2(react-native@0.73.0(@babel/core@7.12.9)(@babel/preset-env@7.12.7(@babel/core@7.12.9))(encoding@0.1.13)(react@18.3.1))': dependencies: '@babel/runtime': 7.25.0 @@ -43895,7 +44074,7 @@ snapshots: '@wordpress/warning@2.6.1': {} - '@wordpress/warning@3.6.0': {} + '@wordpress/warning@3.7.0': {} '@wordpress/widgets@3.24.0(@babel/helper-module-imports@7.24.7)(@babel/types@7.25.2)(@types/react@17.0.71)(babel-plugin-macros@3.1.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: @@ -44754,20 +44933,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-jest@26.6.3(@babel/core@7.24.7): - dependencies: - '@babel/core': 7.24.7 - '@jest/transform': 26.6.2 - '@jest/types': 26.6.2 - '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 26.6.2(@babel/core@7.24.7) - chalk: 4.1.2 - graceful-fs: 4.2.11 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - babel-jest@26.6.3(@babel/core@7.25.2): dependencies: '@babel/core': 7.25.2 @@ -44796,20 +44961,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-jest@27.5.1(@babel/core@7.24.7): - dependencies: - '@babel/core': 7.24.7 - '@jest/transform': 27.5.1 - '@jest/types': 27.5.1 - '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 27.5.1(@babel/core@7.24.7) - chalk: 4.1.2 - graceful-fs: 4.2.11 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - babel-jest@27.5.1(@babel/core@7.25.2): dependencies: '@babel/core': 7.25.2 @@ -44914,15 +45065,6 @@ snapshots: schema-utils: 2.7.1 webpack: 5.91.0(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@4.10.0) - babel-loader@8.3.0(@babel/core@7.24.7)(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)): - dependencies: - '@babel/core': 7.24.7 - find-cache-dir: 3.3.2 - loader-utils: 2.0.4 - make-dir: 3.1.0 - schema-utils: 2.7.1 - webpack: 5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0) - babel-loader@8.3.0(@babel/core@7.25.2)(webpack@4.47.0(webpack-cli@3.3.12(webpack@5.89.0))): dependencies: '@babel/core': 7.25.2 @@ -45366,6 +45508,7 @@ snapshots: '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.7) '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.7) + optional: true babel-preset-current-node-syntax@1.0.1(@babel/core@7.25.2): dependencies: @@ -45413,12 +45556,6 @@ snapshots: babel-plugin-jest-hoist: 26.6.2 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.5) - babel-preset-jest@26.6.2(@babel/core@7.24.7): - dependencies: - '@babel/core': 7.24.7 - babel-plugin-jest-hoist: 26.6.2 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.7) - babel-preset-jest@26.6.2(@babel/core@7.25.2): dependencies: '@babel/core': 7.25.2 @@ -45431,12 +45568,6 @@ snapshots: babel-plugin-jest-hoist: 27.5.1 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.5) - babel-preset-jest@27.5.1(@babel/core@7.24.7): - dependencies: - '@babel/core': 7.24.7 - babel-plugin-jest-hoist: 27.5.1 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.7) - babel-preset-jest@27.5.1(@babel/core@7.25.2): dependencies: '@babel/core': 7.25.2 @@ -48407,7 +48538,7 @@ snapshots: eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.56.0(eslint@8.55.0)(typescript@5.3.2))(eslint-import-resolver-webpack@0.13.2)(eslint-plugin-import@2.28.1)(eslint@8.55.0): dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) enhanced-resolve: 5.15.0 eslint: 8.55.0 eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.56.0(eslint@8.55.0)(typescript@5.3.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.56.0(eslint@8.55.0)(typescript@5.3.2))(eslint-import-resolver-webpack@0.13.2)(eslint-plugin-import@2.28.1)(eslint@8.55.0))(eslint-import-resolver-webpack@0.13.2(eslint-plugin-import@2.28.1)(webpack@5.91.0(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@5.1.4)))(eslint@8.55.0) @@ -48424,7 +48555,7 @@ snapshots: eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.55.0)(typescript@5.3.3))(eslint-import-resolver-webpack@0.13.8)(eslint-plugin-import@2.29.0)(eslint@8.55.0): dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) enhanced-resolve: 5.15.0 eslint: 8.55.0 eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.55.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.55.0)(typescript@5.3.3))(eslint-import-resolver-webpack@0.13.8)(eslint-plugin-import@2.29.0)(eslint@8.55.0))(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.0)(webpack@5.89.0(webpack-cli@4.10.0)))(eslint@8.55.0) @@ -49832,7 +49963,7 @@ snapshots: follow-redirects@1.15.6(debug@4.3.4): optionalDependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) follow-redirects@1.5.10: dependencies: @@ -53025,9 +53156,7 @@ snapshots: pretty-format: 24.9.0 throat: 4.1.0 transitivePeerDependencies: - - bufferutil - supports-color - - utf-8-validate jest-jasmine2@25.5.4: dependencies: @@ -54699,7 +54828,7 @@ snapshots: chalk: 5.2.0 cli-truncate: 3.1.0 commander: 10.0.1 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) execa: 7.2.0 lilconfig: 2.1.0 listr2: 5.0.8(enquirer@2.4.1) @@ -55996,7 +56125,7 @@ snapshots: dependencies: carlo: 0.9.46 chokidar: 3.5.3 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) isbinaryfile: 3.0.3 mime: 2.6.0 opn: 5.5.0 @@ -56524,7 +56653,7 @@ snapshots: '@oclif/plugin-warn-if-update-available': 2.1.1(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3) aws-sdk: 2.1515.0 concurrently: 7.6.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) find-yarn-workspace-root: 2.0.0 fs-extra: 8.1.0 github-slugger: 1.5.0 @@ -58034,7 +58163,7 @@ snapshots: puppeteer-core@13.7.0(encoding@0.1.13): dependencies: cross-fetch: 3.1.5(encoding@0.1.13) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) devtools-protocol: 0.0.981744 extract-zip: 2.0.1 https-proxy-agent: 5.0.1 @@ -58073,7 +58202,7 @@ snapshots: '@puppeteer/browsers': 1.4.6(typescript@5.3.2) chromium-bidi: 0.4.16(devtools-protocol@0.0.1147663) cross-fetch: 4.0.0(encoding@0.1.13) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) devtools-protocol: 0.0.1147663 ws: 8.13.0 optionalDependencies: @@ -58089,7 +58218,7 @@ snapshots: '@puppeteer/browsers': 1.4.6(typescript@5.3.3) chromium-bidi: 0.4.16(devtools-protocol@0.0.1147663) cross-fetch: 4.0.0(encoding@0.1.13) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) devtools-protocol: 0.0.1147663 ws: 8.13.0 optionalDependencies: @@ -58105,7 +58234,7 @@ snapshots: '@puppeteer/browsers': 1.9.0 chromium-bidi: 0.5.1(devtools-protocol@0.0.1203626) cross-fetch: 4.0.0(encoding@0.1.13) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) devtools-protocol: 0.0.1203626 ws: 8.14.2 transitivePeerDependencies: @@ -58141,7 +58270,7 @@ snapshots: puppeteer@17.1.3(encoding@0.1.13): dependencies: cross-fetch: 3.1.5(encoding@0.1.13) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) devtools-protocol: 0.0.1036444 extract-zip: 2.0.1 https-proxy-agent: 5.0.1 @@ -58469,7 +58598,7 @@ snapshots: react-docgen-typescript-plugin@1.0.5(typescript@5.3.2)(webpack@5.91.0(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@5.1.4)): dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -60346,7 +60475,7 @@ snapshots: dependencies: '@kwsites/file-exists': 1.1.1 '@kwsites/promise-deferred': 1.1.1 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) transitivePeerDependencies: - supports-color @@ -61095,7 +61224,7 @@ snapshots: colord: 2.9.3 cosmiconfig: 7.1.0 css-functions-list: 3.2.1 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@9.4.0) fast-glob: 3.3.2 fastest-levenshtein: 1.0.16 file-entry-cache: 6.0.1 @@ -61899,7 +62028,7 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.24.7) - ts-jest@29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3): + ts-jest@29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -63005,8 +63134,8 @@ snapshots: webpack-cli@4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 1.2.0(webpack-cli@4.10.0(webpack@5.89.0))(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) - '@webpack-cli/info': 1.5.0(webpack-cli@4.10.0(webpack@5.89.0)) + '@webpack-cli/configtest': 1.2.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0))(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + '@webpack-cli/info': 1.5.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0)) '@webpack-cli/serve': 1.7.0(webpack-cli@4.10.0(webpack@5.89.0)) colorette: 2.0.20 commander: 7.2.0 From ec9cdbdbf79bce9a8ad867d09fb9369c23a9a505 Mon Sep 17 00:00:00 2001 From: "Veljko V." Date: Thu, 19 Sep 2024 11:14:20 +0200 Subject: [PATCH 20/20] Fix and improve k6 perf test My Account Open Order (#51334) * Add customer id to orders and update test to look for proper title * Add changelog * Update my account open orders test and assign customer ID * Fix linting issues * Add final fix to the my account open order test * Add customer order to test * Add comment on the top of the file * Add two more comments to the top of the file to avoid linting failure * Revert changes made to response in test and orders * Rename response to r in orders * Remove unnecessary code from orders * Move order creation to setup and update tests * Include check for the order ID validation * Relogic add customer order * Rename test comment * Update setup to create customer and config, readme, relevant tests * Remove console log from the test --- ...erformance-fix-my-account-open-orders-test | 4 + .../woocommerce/tests/performance/README.md | 1 + .../woocommerce/tests/performance/config.js | 7 +- .../tests/performance/requests/api/orders.js | 28 +++--- .../shopper/checkout-customer-login.js | 4 +- .../requests/shopper/my-account-orders.js | 12 ++- .../requests/shopper/my-account.js | 4 +- .../performance/setup/add-customer-order.js | 88 +++++++++++++++++++ .../setup/cart-checkout-shortcode.js | 6 +- .../tests/gh-action-daily-ext-requests.js | 5 ++ .../tests/gh-action-pr-requests.js | 13 ++- .../performance/tests/hpos-baseline-load.js | 12 ++- .../performance/tests/simple-all-requests.js | 5 ++ .../performance/tests/wc-baseline-load.js | 5 ++ .../tests/wc-regression-test-load.js | 5 ++ 15 files changed, 171 insertions(+), 28 deletions(-) create mode 100644 plugins/woocommerce/changelog/k6-performance-fix-my-account-open-orders-test create mode 100644 plugins/woocommerce/tests/performance/setup/add-customer-order.js diff --git a/plugins/woocommerce/changelog/k6-performance-fix-my-account-open-orders-test b/plugins/woocommerce/changelog/k6-performance-fix-my-account-open-orders-test new file mode 100644 index 00000000000..ba1449e0385 --- /dev/null +++ b/plugins/woocommerce/changelog/k6-performance-fix-my-account-open-orders-test @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix k6 performance test that checks for incorrect title when viewing order page diff --git a/plugins/woocommerce/tests/performance/README.md b/plugins/woocommerce/tests/performance/README.md index d5554c63035..8cb416a6101 100644 --- a/plugins/woocommerce/tests/performance/README.md +++ b/plugins/woocommerce/tests/performance/README.md @@ -75,6 +75,7 @@ admin_username | username for admin user | yes `__ENV.A_USER` admin_password | password for admin user | yes `__ENV.A_PW` admin_acc_login | set to true if site needs to use my account for admin login | yes `__ENV.A_ACC_LOGIN` customer_username | username for customer user | yes `__ENV.C_USER` +customer_email | email for customer user | yes `__ENV.C_EMAIL` customer_password | password for customer user | yes `__ENV.C_PW` customer_user_id | user id for customer user | yes `__ENV.C_UID` hpos_status | set to true if site is using order tables | yes `__ENV.HPOS` diff --git a/plugins/woocommerce/tests/performance/config.js b/plugins/woocommerce/tests/performance/config.js index d4638940de0..6b9db4491c5 100644 --- a/plugins/woocommerce/tests/performance/config.js +++ b/plugins/woocommerce/tests/performance/config.js @@ -9,10 +9,13 @@ export const admin_username = __ENV.A_USER || 'admin'; export const admin_password = __ENV.A_PW || 'password'; export const admin_acc_login = __ENV.A_ACC_LOGIN || false; -export const customer_username = - __ENV.C_USER || 'customer@woocommercecoree2etestsuite.com'; +export const customer_username = __ENV.C_USER || 'customer'; +export const customer_email = + __ENV.C_EMAIL || 'customer@woocommercecoree2etestsuite.com'; export const customer_password = __ENV.C_PW || 'password'; export const customer_user_id = __ENV.C_UID || '2'; +export const customer_first_name = 'Jane'; +export const customer_last_name = 'Smith'; export const hpos_status = __ENV.HPOS || false; diff --git a/plugins/woocommerce/tests/performance/requests/api/orders.js b/plugins/woocommerce/tests/performance/requests/api/orders.js index 26ca0fb6a98..f3913a93bc9 100644 --- a/plugins/woocommerce/tests/performance/requests/api/orders.js +++ b/plugins/woocommerce/tests/performance/requests/api/orders.js @@ -84,8 +84,8 @@ export function ordersAPI() { ); check( response, { 'status is 201': ( r ) => r.status === 201, - "body contains: 'Pending' Status": ( response ) => - response.body.includes( '"status":"pending"' ), + "body contains: 'Pending' Status": ( r ) => + r.body.includes( '"status":"pending"' ), } ); post_id = findBetween( response.body, '{"id":', ',' ); @@ -102,8 +102,8 @@ export function ordersAPI() { ); check( response, { 'status is 200': ( r ) => r.status === 200, - 'body contains: Order ID': ( response ) => - response.body.includes( `"id":${ post_id }` ), + 'body contains: Order ID': ( r ) => + r.body.includes( `"id":${ post_id }` ), } ); } } ); @@ -116,8 +116,8 @@ export function ordersAPI() { } ); check( response, { 'status is 200': ( r ) => r.status === 200, - 'body contains: Order ID': ( response ) => - response.body.includes( '[{"id":' ), + 'body contains: Order ID': ( r ) => + r.body.includes( '[{"id":' ), } ); } } ); @@ -134,8 +134,8 @@ export function ordersAPI() { ); check( response, { 'status is 200': ( r ) => r.status === 200, - "body contains: 'Completed' Status": ( response ) => - response.body.includes( '"status":"completed"' ), + "body contains: 'Completed' Status": ( r ) => + r.body.includes( '"status":"completed"' ), } ); } } ); @@ -152,8 +152,8 @@ export function ordersAPI() { ); check( response, { 'status is 200': ( r ) => r.status === 200, - 'body contains: Order ID': ( response ) => - response.body.includes( `"id":${ post_id }` ), + 'body contains: Order ID': ( r ) => + r.body.includes( `"id":${ post_id }` ), } ); } } ); @@ -176,8 +176,8 @@ export function ordersAPI() { ); check( response, { 'status is 200': ( r ) => r.status === 200, - 'body contains: Create batch prefix': ( response ) => - response.body.includes( 'create":[{"id"' ), + 'body contains: Create batch prefix': ( r ) => + r.body.includes( 'create":[{"id"' ), } ); post_ids = findBetween( response.body, '{"id":', ',"parent_id', true ); @@ -213,8 +213,8 @@ export function ordersAPI() { ); check( response, { 'status is 200': ( r ) => r.status === 200, - 'body contains: Update batch prefix': ( response ) => - response.body.includes( 'update":[{"id"' ), + 'body contains: Update batch prefix': ( r ) => + r.body.includes( 'update":[{"id"' ), } ); } ); } diff --git a/plugins/woocommerce/tests/performance/requests/shopper/checkout-customer-login.js b/plugins/woocommerce/tests/performance/requests/shopper/checkout-customer-login.js index 4be19fc4824..f4dc42d257b 100644 --- a/plugins/woocommerce/tests/performance/requests/shopper/checkout-customer-login.js +++ b/plugins/woocommerce/tests/performance/requests/shopper/checkout-customer-login.js @@ -14,7 +14,7 @@ import { */ import { base_url, - customer_username, + customer_email, customer_password, addresses_customer_billing_first_name, addresses_customer_billing_last_name, @@ -139,7 +139,7 @@ export function checkoutCustomerLogin() { const response = http.post( `${ base_url }/checkout`, { - username: `${ customer_username }`, + username: `${ customer_email }`, password: `${ customer_password }`, 'woocommerce-login-nonce': `${ woocommerce_login_nonce }`, _wp_http_referer: '%2Fcheckout', diff --git a/plugins/woocommerce/tests/performance/requests/shopper/my-account-orders.js b/plugins/woocommerce/tests/performance/requests/shopper/my-account-orders.js index 789a3eb185f..f257e1890d5 100644 --- a/plugins/woocommerce/tests/performance/requests/shopper/my-account-orders.js +++ b/plugins/woocommerce/tests/performance/requests/shopper/my-account-orders.js @@ -2,7 +2,7 @@ /** * External dependencies */ -import { sleep, group } from 'k6'; +import { sleep, group, check } from 'k6'; import http from 'k6/http'; import { randomIntBetween, @@ -98,9 +98,15 @@ export function myAccountOrders() { } ); + check( my_account_order_id, { + 'order ID is not undefined': () => { + return !! my_account_order_id; + }, + } ); + checkResponse( response, 200, { - title: `My account – ${ STORE_NAME }`, - body: my_account_order_id, + title: `Order #${ my_account_order_id } – ${ STORE_NAME }`, + body: `Order #${ my_account_order_id } was placed`, footer: FOOTER_TEXT, } ); } ); diff --git a/plugins/woocommerce/tests/performance/requests/shopper/my-account.js b/plugins/woocommerce/tests/performance/requests/shopper/my-account.js index ec1f70c930b..1f4def3b77d 100644 --- a/plugins/woocommerce/tests/performance/requests/shopper/my-account.js +++ b/plugins/woocommerce/tests/performance/requests/shopper/my-account.js @@ -11,7 +11,7 @@ import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.1.0/index.js'; */ import { base_url, - customer_username, + customer_email, customer_password, think_time_min, think_time_max, @@ -74,7 +74,7 @@ export function myAccount() { response = http.post( `${ base_url }/my-account`, { - username: `${ customer_username }`, + username: `${ customer_email }`, password: `${ customer_password }`, 'woocommerce-login-nonce': `${ woocommerce_login_nonce }`, _wp_http_referer: '/my-account', diff --git a/plugins/woocommerce/tests/performance/setup/add-customer-order.js b/plugins/woocommerce/tests/performance/setup/add-customer-order.js new file mode 100644 index 00000000000..543e27c8e90 --- /dev/null +++ b/plugins/woocommerce/tests/performance/setup/add-customer-order.js @@ -0,0 +1,88 @@ +/* eslint-disable jsdoc/require-property-description */ +/* eslint-disable @woocommerce/dependency-group */ +/* eslint-disable import/no-unresolved */ +/** + * k6 dependencies + */ +import encoding from 'k6/encoding'; +import http from 'k6/http'; + +/** + * Internal dependencies + */ +import { + base_url, + admin_username, + admin_password, + customer_username, + customer_email, + customer_password, + customer_first_name, + customer_last_name, +} from '../config.js'; + +/** + * Add a customer order specifically for my-account-orders.js test + */ +export function addCustomerOrder() { + let response; + const customerUsername = 'customer'; + + const credentials = `${ admin_username }:${ admin_password }`; + const encodedCredentials = encoding.b64encode( credentials ); + const requestHeaders = { + Authorization: `Basic ${ encodedCredentials }`, + 'Content-Type': 'application/json', + }; + + // Fetch the list of customers + response = http.get( `${ base_url }/wp-json/wc/v3/customers`, { + headers: requestHeaders, + } ); + + // Parse the response body as JSON to find the customer by username + const customers = JSON.parse( response.body ); + let customerId = null; + + for ( let i = 0; i < customers.length; i++ ) { + if ( customers[ i ].username === customerUsername ) { + customerId = customers[ i ].id; + break; + } + } + + // If customer doesn't exist, create the customer + if ( ! customerId ) { + console.log( + `Customer with username ${ customer_username } not found. Creating new customer...` + ); + const createCustomerData = { + username: customer_username, + password: customer_password, + email: customer_email, + first_name: customer_first_name, + last_name: customer_last_name, + }; + + response = http.post( + `${ base_url }/wp-json/wc/v3/customers`, + JSON.stringify( createCustomerData ), + { + headers: requestHeaders, + } + ); + } + + // Create a new order for the identified customer + const createCustomerOrderData = { + customer_id: customerId, + status: 'completed', + }; + response = http.post( + `${ base_url }/wp-json/wc/v3/orders`, + JSON.stringify( createCustomerOrderData ), + { + headers: requestHeaders, + } + ); +} diff --git a/plugins/woocommerce/tests/performance/setup/cart-checkout-shortcode.js b/plugins/woocommerce/tests/performance/setup/cart-checkout-shortcode.js index eaa5204ef70..77f61655366 100644 --- a/plugins/woocommerce/tests/performance/setup/cart-checkout-shortcode.js +++ b/plugins/woocommerce/tests/performance/setup/cart-checkout-shortcode.js @@ -1,3 +1,6 @@ +/* eslint-disable jsdoc/require-property-description */ +/* eslint-disable @woocommerce/dependency-group */ +/* eslint-disable import/no-unresolved */ /** * k6 dependencies */ @@ -13,9 +16,10 @@ import { base_url, admin_username, admin_password } from '../config.js'; /** * Convert Cart & Checkout pages to shortcode. */ -export function useCartCheckoutShortcodes() { +export function setCartCheckoutShortcodes() { /** * A WordPress page. + * * @typedef {Object} WPPage * @property {number} id * @property {string} slug diff --git a/plugins/woocommerce/tests/performance/tests/gh-action-daily-ext-requests.js b/plugins/woocommerce/tests/performance/tests/gh-action-daily-ext-requests.js index 7ecec81086e..f0ce502d2c2 100644 --- a/plugins/woocommerce/tests/performance/tests/gh-action-daily-ext-requests.js +++ b/plugins/woocommerce/tests/performance/tests/gh-action-daily-ext-requests.js @@ -21,6 +21,7 @@ import { ordersSearch } from '../requests/merchant/orders-search.js'; import { addOrder } from '../requests/merchant/add-order.js'; import { ordersAPI } from '../requests/api/orders.js'; import { homeWCAdmin } from '../requests/merchant/home-wc-admin.js'; +import { addCustomerOrder } from '../setup/add-customer-order.js'; const shopper_request_threshold = 'p(95)<10000'; const merchant_request_threshold = 'p(95)<10000'; @@ -247,6 +248,10 @@ export const options = { }, }; +export function setup() { + addCustomerOrder(); +} + export function shopperBrowseFlow() { homePage(); shopPage(); diff --git a/plugins/woocommerce/tests/performance/tests/gh-action-pr-requests.js b/plugins/woocommerce/tests/performance/tests/gh-action-pr-requests.js index c06aed22d2b..37780d605dc 100644 --- a/plugins/woocommerce/tests/performance/tests/gh-action-pr-requests.js +++ b/plugins/woocommerce/tests/performance/tests/gh-action-pr-requests.js @@ -22,7 +22,8 @@ import { ordersFilter } from '../requests/merchant/orders-filter.js'; import { addOrder } from '../requests/merchant/add-order.js'; import { ordersAPI } from '../requests/api/orders.js'; import { homeWCAdmin } from '../requests/merchant/home-wc-admin.js'; -import { useCartCheckoutShortcodes } from '../setup/cart-checkout-shortcode.js'; +import { setCartCheckoutShortcodes } from '../setup/cart-checkout-shortcode.js'; +import { addCustomerOrder } from '../setup/add-customer-order.js'; const shopper_request_threshold = 'p(95)<10000'; const merchant_request_threshold = 'p(95)<10000'; @@ -250,7 +251,8 @@ export const options = { }; export function setup() { - useCartCheckoutShortcodes(); + setCartCheckoutShortcodes(); + addCustomerOrder(); } export function shopperBrowseFlow() { @@ -277,7 +279,12 @@ export function cartFlow() { } export function allMerchantFlow() { wpLogin(); - homeWCAdmin( { other: false, orders: false, reviews: false, products: false} ); + homeWCAdmin( { + other: false, + orders: false, + reviews: false, + products: false, + } ); addOrder(); orders(); ordersSearch(); diff --git a/plugins/woocommerce/tests/performance/tests/hpos-baseline-load.js b/plugins/woocommerce/tests/performance/tests/hpos-baseline-load.js index 17a937d903b..291a2e4e2ec 100644 --- a/plugins/woocommerce/tests/performance/tests/hpos-baseline-load.js +++ b/plugins/woocommerce/tests/performance/tests/hpos-baseline-load.js @@ -25,6 +25,7 @@ import { myAccountMerchantLogin } from '../requests/merchant/my-account-merchant import { wpLogin } from '../requests/merchant/wp-login.js'; import { ordersAPI } from '../requests/api/orders.js'; import { admin_acc_login } from '../config.js'; +import { addCustomerOrder } from '../setup/add-customer-order.js'; const shopper_request_threshold = 'p(95)<100000'; const merchant_request_threshold = 'p(95)<100000'; @@ -256,6 +257,10 @@ export const options = { }, }; +export function setup() { + addCustomerOrder(); +} + // Use myAccountMerchantLogin() instead of wpLogin() if having issues with login. export function merchantOrderFlows() { if ( admin_acc_login === true ) { @@ -277,7 +282,12 @@ export function merchantOtherFlows() { wpLogin(); } homeWCAdmin( { other: false, products: false, reviews: false } ); - addProduct( { heartbeat: false, other: false, permalink: false, update: false } ); + addProduct( { + heartbeat: false, + other: false, + permalink: false, + update: false, + } ); coupons(); } export function shopperBrowsingFlows() { diff --git a/plugins/woocommerce/tests/performance/tests/simple-all-requests.js b/plugins/woocommerce/tests/performance/tests/simple-all-requests.js index 52c79b9dc52..172503d45fe 100644 --- a/plugins/woocommerce/tests/performance/tests/simple-all-requests.js +++ b/plugins/woocommerce/tests/performance/tests/simple-all-requests.js @@ -25,6 +25,7 @@ import { wpLogin } from '../requests/merchant/wp-login.js'; import { myAccountMerchantLogin } from '../requests/merchant/my-account-merchant.js'; import { ordersAPI } from '../requests/api/orders.js'; import { admin_acc_login } from '../config.js'; +import { addCustomerOrder } from '../setup/add-customer-order.js'; const shopper_request_threshold = 'p(95)<10000'; const merchant_request_threshold = 'p(95)<10000'; @@ -251,6 +252,10 @@ export const options = { }, }; +export function setup() { + addCustomerOrder(); +} + export function shopperBrowseFlow() { homePage(); shopPage(); diff --git a/plugins/woocommerce/tests/performance/tests/wc-baseline-load.js b/plugins/woocommerce/tests/performance/tests/wc-baseline-load.js index cd39d327319..988cf49b16c 100644 --- a/plugins/woocommerce/tests/performance/tests/wc-baseline-load.js +++ b/plugins/woocommerce/tests/performance/tests/wc-baseline-load.js @@ -26,6 +26,7 @@ import { myAccountMerchantLogin } from '../requests/merchant/my-account-merchant import { wpLogin } from '../requests/merchant/wp-login.js'; import { ordersAPI } from '../requests/api/orders.js'; import { admin_acc_login } from '../config.js'; +import { addCustomerOrder } from '../setup/add-customer-order.js'; const shopper_request_threshold = 'p(95)<100000'; const merchant_request_threshold = 'p(95)<100000'; @@ -282,6 +283,10 @@ export const options = { }, }; +export function setup() { + addCustomerOrder(); +} + // Use myAccountMerchantLogin() instead of wpLogin() if having issues with login. export function merchantOrderFlows() { if ( admin_acc_login === true ) { diff --git a/plugins/woocommerce/tests/performance/tests/wc-regression-test-load.js b/plugins/woocommerce/tests/performance/tests/wc-regression-test-load.js index 57a4698feff..69c9d33d52d 100644 --- a/plugins/woocommerce/tests/performance/tests/wc-regression-test-load.js +++ b/plugins/woocommerce/tests/performance/tests/wc-regression-test-load.js @@ -26,6 +26,7 @@ import { myAccountMerchantLogin } from '../requests/merchant/my-account-merchant import { wpLogin } from '../requests/merchant/wp-login.js'; import { ordersAPI } from '../requests/api/orders.js'; import { admin_acc_login } from '../config.js'; +import { addCustomerOrder } from '../setup/add-customer-order.js'; const shopper_request_threshold = 'p(95)<100000'; const merchant_request_threshold = 'p(95)<100000'; @@ -282,6 +283,10 @@ export const options = { }, }; +export function setup() { + addCustomerOrder(); +} + // Use myAccountMerchantLogin() instead of wpLogin() if having issues with login. export function merchantOrderFlows() { if ( admin_acc_login === true ) {