From 7971df1d280fe1da1cf1c04602be0db314928f80 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 30 Aug 2024 20:25:52 +0800 Subject: [PATCH 001/187] Enhance WooCommerce version checking for remote logging reliability (#51009) * Enhance WooCommerce version checking using get_plugin_updates() * Update remote logger tool to toggle remote logging feature properly * Add changelog --- .../api/remote-logging/remote-logging.php | 1 + .../changelog/update-move-woo-version-check | 4 + .../changelog/update-move-woo-version-check | 4 + .../src/Internal/Logging/RemoteLogger.php | 74 +++++----- .../src/Internal/Logging/RemoteLoggerTest.php | 129 +++++++++++++----- 5 files changed, 141 insertions(+), 71 deletions(-) create mode 100644 plugins/woocommerce-beta-tester/changelog/update-move-woo-version-check create mode 100644 plugins/woocommerce/changelog/update-move-woo-version-check diff --git a/plugins/woocommerce-beta-tester/api/remote-logging/remote-logging.php b/plugins/woocommerce-beta-tester/api/remote-logging/remote-logging.php index 6e7d9b1487b..80d518e03f2 100644 --- a/plugins/woocommerce-beta-tester/api/remote-logging/remote-logging.php +++ b/plugins/woocommerce-beta-tester/api/remote-logging/remote-logging.php @@ -74,6 +74,7 @@ function toggle_remote_logging( $request ) { update_option( 'woocommerce_feature_remote_logging_enabled', 'yes' ); update_option( 'woocommerce_allow_tracking', 'yes' ); update_option( 'woocommerce_remote_variant_assignment', 1 ); + set_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT, WC()->version ); } else { update_option( 'woocommerce_feature_remote_logging_enabled', 'no' ); } diff --git a/plugins/woocommerce-beta-tester/changelog/update-move-woo-version-check b/plugins/woocommerce-beta-tester/changelog/update-move-woo-version-check new file mode 100644 index 00000000000..6807f1d962f --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/update-move-woo-version-check @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update remote logger tool to toggle remote logging feature properly diff --git a/plugins/woocommerce/changelog/update-move-woo-version-check b/plugins/woocommerce/changelog/update-move-woo-version-check new file mode 100644 index 00000000000..af5e8c0070e --- /dev/null +++ b/plugins/woocommerce/changelog/update-move-woo-version-check @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Enhance WooCommerce version checking for remote logging reliability diff --git a/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php b/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php index 01e71d40e62..3bcb44f4c29 100644 --- a/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php +++ b/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php @@ -20,11 +20,10 @@ use WC_Log_Levels; * @package WooCommerce\Classes */ class RemoteLogger extends \WC_Log_Handler { - const LOG_ENDPOINT = 'https://public-api.wordpress.com/rest/v1.1/logstash'; - const RATE_LIMIT_ID = 'woocommerce_remote_logging'; - const RATE_LIMIT_DELAY = 60; // 1 minute. - const WC_LATEST_VERSION_TRANSIENT = 'latest_woocommerce_version'; - const FETCH_LATEST_VERSION_RETRY = 'fetch_latest_woocommerce_version_retry'; + const LOG_ENDPOINT = 'https://public-api.wordpress.com/rest/v1.1/logstash'; + const RATE_LIMIT_ID = 'woocommerce_remote_logging'; + const RATE_LIMIT_DELAY = 60; // 1 minute. + const WC_NEW_VERSION_TRANSIENT = 'woocommerce_new_version'; /** * Handle a log entry. @@ -150,7 +149,7 @@ class RemoteLogger extends \WC_Log_Handler { return false; } - if ( ! $this->is_latest_woocommerce_version() ) { + if ( ! $this->should_current_version_be_logged() ) { return false; } @@ -221,7 +220,7 @@ class RemoteLogger extends \WC_Log_Handler { self::LOG_ENDPOINT, array( 'body' => wp_json_encode( $body ), - 'timeout' => 2, + 'timeout' => 3, 'headers' => array( 'Content-Type' => 'application/json', ), @@ -256,14 +255,22 @@ class RemoteLogger extends \WC_Log_Handler { * * @return bool */ - private function is_latest_woocommerce_version() { - $latest_wc_version = $this->fetch_latest_woocommerce_version(); + private function should_current_version_be_logged() { + $new_version = get_site_transient( self::WC_NEW_VERSION_TRANSIENT ); - if ( is_null( $latest_wc_version ) ) { - return false; + if ( false === $new_version ) { + $new_version = $this->fetch_new_woocommerce_version(); + // Cache the new version for a week since we want to keep logging in with the same version for a while even if the new version is available. + set_site_transient( self::WC_NEW_VERSION_TRANSIENT, $new_version, WEEK_IN_SECONDS ); } - return version_compare( WC()->version, $latest_wc_version, '>=' ); + if ( ! is_string( $new_version ) || '' === $new_version ) { + // If the new version is not available, we consider the current version to be the latest. + return true; + } + + // If the current version is the latest, we don't want to log errors. + return version_compare( WC()->version, $new_version, '>=' ); } /** @@ -316,45 +323,34 @@ class RemoteLogger extends \WC_Log_Handler { } /** - * Fetch the latest WooCommerce version using the WordPress API and cache it. + * Fetch the new version of WooCommerce from the WordPress API. * - * @return string|null + * @return string|null New version if an update is available, null otherwise. */ - private function fetch_latest_woocommerce_version() { - $cached_version = get_transient( self::WC_LATEST_VERSION_TRANSIENT ); - if ( $cached_version ) { - return $cached_version; + private function fetch_new_woocommerce_version() { + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + if ( ! function_exists( 'get_plugin_updates' ) ) { + require_once ABSPATH . 'wp-admin/includes/update.php'; } - $retry_count = get_transient( self::FETCH_LATEST_VERSION_RETRY ); - if ( false === $retry_count || ! is_numeric( $retry_count ) ) { - $retry_count = 0; - } + $plugin_updates = get_plugin_updates(); - if ( $retry_count >= 3 ) { + // Check if WooCommerce plugin update information is available. + if ( ! is_array( $plugin_updates ) || ! isset( $plugin_updates[ WC_PLUGIN_BASENAME ] ) ) { return null; } - if ( ! function_exists( 'plugins_api' ) ) { - require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; - } - // Fetch the latest version from the WordPress API. - $plugin_info = plugins_api( 'plugin_information', array( 'slug' => 'woocommerce' ) ); + $wc_plugin_update = $plugin_updates[ WC_PLUGIN_BASENAME ]; - if ( is_wp_error( $plugin_info ) ) { - ++$retry_count; - set_transient( self::FETCH_LATEST_VERSION_RETRY, $retry_count, HOUR_IN_SECONDS ); + // Ensure the update object exists and has the required information. + if ( ! $wc_plugin_update || ! isset( $wc_plugin_update->update->new_version ) ) { return null; } - if ( ! empty( $plugin_info->version ) ) { - $latest_version = $plugin_info->version; - set_transient( self::WC_LATEST_VERSION_TRANSIENT, $latest_version, WEEK_IN_SECONDS ); - delete_transient( self::FETCH_LATEST_VERSION_RETRY ); - return $latest_version; - } - - return null; + $new_version = $wc_plugin_update->update->new_version; + return is_string( $new_version ) ? $new_version : null; } /** diff --git a/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php b/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php index 7133f38f5ef..63327438a03 100644 --- a/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php +++ b/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php @@ -35,8 +35,7 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case { public function tearDown(): void { $this->cleanup_filters(); delete_option( 'woocommerce_feature_remote_logging_enabled' ); - delete_transient( RemoteLogger::WC_LATEST_VERSION_TRANSIENT ); - delete_transient( RemoteLogger::FETCH_LATEST_VERSION_RETRY ); + delete_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT ); global $wpdb; $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_rate_limits" ); WC_Cache_Helper::invalidate_cache_group( WC_Rate_Limiter::CACHE_GROUP ); @@ -56,6 +55,7 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case { 'plugins_api', 'pre_http_request', 'woocommerce_remote_logger_formatted_log_data', + 'pre_site_transient_update_plugins', ); foreach ( $filters as $filter ) { remove_all_filters( $filter ); @@ -90,18 +90,23 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case { */ public function remote_logging_disallowed_provider() { return array( - 'feature flag disabled' => array( + 'feature flag disabled' => array( 'condition' => 'feature flag disabled', 'setup' => fn() => update_option( 'woocommerce_feature_remote_logging_enabled', 'no' ), ), - 'tracking opted out' => array( + 'tracking opted out' => array( 'condition' => 'tracking opted out', 'setup' => fn() => add_filter( 'option_woocommerce_allow_tracking', fn() => 'no' ), ), - 'outdated version' => array( - 'condition' => 'outdated version', - 'setup' => function () { + 'high variant assignment' => array( + 'condition' => 'high variant assignment', + 'setup' => fn() => add_filter( 'option_woocommerce_remote_variant_assignment', fn() => 15 ), + ), + 'outdated version' => array( + 'condition' => 'outdated version', + 'setup' => function () { $version = WC()->version; + // Next major version. (e.g. 9.0.1 -> 10.0.0). $next_version = implode( '.', array_map( @@ -112,28 +117,79 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case { array_keys( explode( '.', $version ) ) ) ); - set_transient( RemoteLogger::WC_LATEST_VERSION_TRANSIENT, $next_version ); + + set_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT, $next_version, WEEK_IN_SECONDS ); }, - 'high variant assignment' => array( - 'condition' => 'high variant assignment', - 'setup' => fn() => add_filter( 'option_woocommerce_remote_variant_assignment', fn() => 15 ), - ), ), ); } - /** - * @testdox Fetch latest WooCommerce version retries on API failure - */ - public function test_fetch_latest_woocommerce_version_retry() { - $this->setup_remote_logging_conditions( true ); - add_filter( 'plugins_api', fn() => new \WP_Error(), 10, 3 ); - for ( $i = 1; $i <= 4; $i++ ) { - $this->sut->is_remote_logging_allowed(); - $retry_count = get_transient( RemoteLogger::FETCH_LATEST_VERSION_RETRY ); - $this->assertEquals( min( $i, 3 ), $retry_count ); + /** + * @testdox should_current_version_be_logged method behaves correctly + * @dataProvider should_current_version_be_logged_provider + * + * @param string $current_version The current WooCommerce version. + * @param string $new_version The new WooCommerce version. + * @param string $transient_value The value of the transient. + * @param bool $expected The expected result. + */ + public function test_should_current_version_be_logged( $current_version, $new_version, $transient_value, $expected ) { + $wc_version = WC()->version; + WC()->version = $current_version; + + // Set up the transient. + if ( null !== $transient_value ) { + set_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT, $transient_value, WEEK_IN_SECONDS ); + } else { + delete_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT ); + + $this->setup_mock_plugin_updates( $new_version ); } + + $result = $this->invoke_private_method( $this->sut, 'should_current_version_be_logged', array() ); + $this->assertEquals( $expected, $result ); + + // Clean up. + delete_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT ); + + WC()->version = $wc_version; + } + + /** + * Data provider for test_should_current_version_be_logged. + */ + public function should_current_version_be_logged_provider() { + return array( + 'current version is latest (transient set)' => array( '9.2.0', '9.2.0', '9.2.0', true ), + 'current version is newer (transient set)' => array( '9.3.0', '9.2.0', '9.2.0', true ), + 'current version is older (transient set)' => array( '9.1.0', '9.2.0', '9.2.0', false ), + 'new version is null (transient set)' => array( '9.2.0', null, null, true ), + 'transient not set, current version is latest' => array( '9.2.0', '9.2.0', null, true ), + 'transient not set, current version is newer' => array( '9.3.0', '9.2.0', null, true ), + 'transient not set, current version is older' => array( '9.1.0', '9.2.0', null, false ), + 'transient not set, new version is null' => array( '9.2.0', null, null, true ), + ); + } + + /** + * @testdox fetch_new_woocommerce_version method returns correct version + */ + public function test_fetch_new_woocommerce_version() { + $this->setup_mock_plugin_updates( '9.3.0' ); + + $result = $this->invoke_private_method( $this->sut, 'fetch_new_woocommerce_version', array() ); + $this->assertEquals( '9.3.0', $result, 'The result should be the latest version when an update is available.' ); + } + + /** + * @testdox fetch_new_woocommerce_version method returns null when no update is available + */ + public function test_fetch_new_woocommerce_version_no_update() { + add_filter( 'pre_site_transient_update_plugins', fn() => array() ); + + $result = $this->invoke_private_method( $this->sut, 'fetch_new_woocommerce_version', array() ); + $this->assertNull( $result, 'The result should be null when no update is available.' ); } /** @@ -421,17 +477,26 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case { update_option( 'woocommerce_feature_remote_logging_enabled', $enabled ? 'yes' : 'no' ); add_filter( 'option_woocommerce_allow_tracking', fn() => 'yes' ); add_filter( 'option_woocommerce_remote_variant_assignment', fn() => 5 ); - add_filter( - 'plugins_api', - function ( $result, $action, $args ) use ( $enabled ) { - if ( 'plugin_information' === $action && 'woocommerce' === $args->slug ) { - return (object) array( 'version' => $enabled ? WC()->version : '9.0.0' ); - } - return $result; - }, - 10, - 3 + $this->setup_mock_plugin_updates( $enabled ? WC()->version : '9.0.0' ); + } + + + /** + * Set up mock plugin updates. + * + * @param string $new_version The new version of WooCommerce to simulate. + */ + private function setup_mock_plugin_updates( $new_version ) { + $update_plugins = (object) array( + 'response' => array( + WC_PLUGIN_BASENAME => (object) array( + 'new_version' => $new_version, + 'package' => 'https://downloads.wordpress.org/plugin/woocommerce.zip', + 'slug' => 'woocommerce', + ), + ), ); + add_filter( 'pre_site_transient_update_plugins', fn() => $update_plugins ); } /** From 502b4abe43e7565356bf95c2b8f824c9c8e62534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alba=20Rinc=C3=B3n?= Date: Fri, 30 Aug 2024 14:40:05 +0200 Subject: [PATCH 002/187] Fix increasing & decreasing sale price in product bulk edit (#50842) * Set the regular price when the `Change to:` price is left empty * Avoid fatal error when price is empty * Add changefile(s) from automation for the following project(s): woocommerce * Avoid setting the price to 0 when there wasn't a previous sale * Move the WC_Tests_Admin_Post_Types test to the appropiate folder so it's run * Add e2e test * Add e2e test * Add changefile(s) from automation for the following project(s): woocommerce * Add changefile(s) from automation for the following project(s): woocommerce * Fix lint errors * Fix lint errors * Fix test * Fix lint errors * Revert mv * Address increasing sale from 0 --------- Co-authored-by: github-actions --- ...-43149-fix-bulk-product-price-in-de-crease | 4 + .../admin/class-wc-admin-post-types.php | 3 +- .../tests/merchant/product-edit.spec.js | 116 ++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/50842-43149-fix-bulk-product-price-in-de-crease diff --git a/plugins/woocommerce/changelog/50842-43149-fix-bulk-product-price-in-de-crease b/plugins/woocommerce/changelog/50842-43149-fix-bulk-product-price-in-de-crease new file mode 100644 index 00000000000..affaea6bdf9 --- /dev/null +++ b/plugins/woocommerce/changelog/50842-43149-fix-bulk-product-price-in-de-crease @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Product bulk edit: fix increasing & decreasing sale price when there was no previous sale. \ No newline at end of file diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-post-types.php b/plugins/woocommerce/includes/admin/class-wc-admin-post-types.php index 741fda165cd..095893653ba 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-post-types.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-post-types.php @@ -896,7 +896,8 @@ class WC_Admin_Post_Types { return false; } - $old_price = (float) $product->{"get_{$price_type}_price"}(); + $old_price = $product->{"get_{$price_type}_price"}(); + $old_price = '' === $old_price ? (float) $product->get_regular_price() : (float) $old_price; $price_changed = false; $change_price = absint( $request_data[ "change_{$price_type}_price" ] ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-edit.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-edit.spec.js index 89d18f069f5..e94595088b6 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-edit.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-edit.spec.js @@ -282,3 +282,119 @@ test( } ); } ); + +test( + 'can decrease the sale price if the product was not previously in sale when bulk editing products', + { tag: [ '@gutenberg', '@services' ] }, + async ( { page, products } ) => { + await page.goto( `wp-admin/edit.php?post_type=product` ); + + const salePriceDecrease = 10; + + await test.step( 'Update products with the "Sale > Decrease existing sale price" option', async () => { + await page.goto( `wp-admin/edit.php?post_type=product` ); + + for ( const product of products ) { + await page.getByLabel( `Select ${ product.name }` ).click(); + } + + await page + .locator( '#bulk-action-selector-top' ) + .selectOption( 'Edit' ); + await page.locator( '#doaction' ).click(); + + await page + .locator( 'select[name="change_sale_price"]' ) + .selectOption( + 'Decrease existing sale price by (fixed amount or %):' + ); + await page + .getByPlaceholder( 'Enter sale price ($)' ) + .fill( `${ salePriceDecrease }%` ); + + await page.getByRole( 'button', { name: 'Update' } ).click(); + } ); + + await test.step( 'Verify products have a sale price', async () => { + for ( const product of products ) { + await page.goto( `product/${ product.slug }` ); + + const expectedSalePrice = ( + product.regular_price * + ( 1 - salePriceDecrease / 100 ) + ).toFixed( 2 ); + + await expect + .soft( + await page + .locator( 'ins' ) + .getByText( `$${ expectedSalePrice }` ) + .count() + ) + .toBeGreaterThan( 0 ); + } + } ); + } +); + +test( + 'increasing the sale price from 0 does not change the sale price when bulk editing products', + { tag: [ '@gutenberg', '@services' ] }, + async ( { page, api } ) => { + let product; + await api + .post( 'products', { + id: 0, + name: `Product _${ Date.now() }`, + type: 'simple', + regular_price: '100', + sale_price: '0', + manage_stock: true, + stock_quantity: 10, + stock_status: 'instock', + } ) + .then( ( response ) => { + product = response.data; + } ); + + const salePriceIncrease = 10; + + await test.step( 'Update products with the "Sale > Increase existing sale price" option', async () => { + await page.goto( `wp-admin/edit.php?post_type=product` ); + + await page.getByLabel( `Select ${ product.name }` ).click(); + + await page + .locator( '#bulk-action-selector-top' ) + .selectOption( 'Edit' ); + await page.locator( '#doaction' ).click(); + + await page + .locator( 'select[name="change_sale_price"]' ) + .selectOption( + 'Increase existing sale price by (fixed amount or %):' + ); + + await page + .getByPlaceholder( 'Enter sale price ($)' ) + .fill( `${ salePriceIncrease }%` ); + + await page.getByRole( 'button', { name: 'Update' } ).click(); + } ); + + await test.step( 'Verify products have a sale price', async () => { + await page.goto( `product/${ product.slug }` ); + + const expectedSalePrice = '$0.00'; + + await expect + .soft( + await page + .locator( 'ins' ) + .getByText( expectedSalePrice ) + .count() + ) + .toBeGreaterThan( 0 ); + } ); + } +); From e589fa87e1c685ce3a2b769b1031f5d4fe166fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Fri, 30 Aug 2024 16:59:28 +0200 Subject: [PATCH 003/187] CYS: Mark several classes as internal (#51069) * CYS: Mark several classes as internal * Add changelog file --- plugins/woocommerce/changelog/fix-cys-internal-classes | 4 ++++ .../Admin/Features/OnboardingTasks/Tasks/CustomizeStore.php | 2 ++ plugins/woocommerce/src/Blocks/AI/Configuration.php | 2 ++ plugins/woocommerce/src/Blocks/AI/Connection.php | 2 ++ plugins/woocommerce/src/Blocks/AIContent/ContentProcessor.php | 2 ++ .../woocommerce/src/Blocks/AIContent/PatternsDictionary.php | 2 ++ plugins/woocommerce/src/Blocks/AIContent/PatternsHelper.php | 2 ++ plugins/woocommerce/src/Blocks/AIContent/UpdatePatterns.php | 2 ++ plugins/woocommerce/src/Blocks/AIContent/UpdateProducts.php | 2 ++ plugins/woocommerce/src/Blocks/Patterns/AIPatterns.php | 2 ++ plugins/woocommerce/src/Blocks/Patterns/PTKClient.php | 2 ++ plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php | 2 ++ plugins/woocommerce/src/Blocks/Patterns/PatternRegistry.php | 2 ++ 13 files changed, 28 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-cys-internal-classes diff --git a/plugins/woocommerce/changelog/fix-cys-internal-classes b/plugins/woocommerce/changelog/fix-cys-internal-classes new file mode 100644 index 00000000000..0bbe2f28b7d --- /dev/null +++ b/plugins/woocommerce/changelog/fix-cys-internal-classes @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Mark several Customize Your Store PHP classes as internal diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/CustomizeStore.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/CustomizeStore.php index fe6a2f28b7a..91fc334318f 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/CustomizeStore.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/CustomizeStore.php @@ -7,6 +7,8 @@ use WP_Post; /** * Customize Your Store Task + * + * @internal */ class CustomizeStore extends Task { /** diff --git a/plugins/woocommerce/src/Blocks/AI/Configuration.php b/plugins/woocommerce/src/Blocks/AI/Configuration.php index cd60cdd7f25..426a672db06 100644 --- a/plugins/woocommerce/src/Blocks/AI/Configuration.php +++ b/plugins/woocommerce/src/Blocks/AI/Configuration.php @@ -8,6 +8,8 @@ use Automattic\Jetpack\Connection\Utils; /** * Class Configuration + * + * @internal */ class Configuration { diff --git a/plugins/woocommerce/src/Blocks/AI/Connection.php b/plugins/woocommerce/src/Blocks/AI/Connection.php index cedd08b8cd2..0a20ddf10f8 100644 --- a/plugins/woocommerce/src/Blocks/AI/Connection.php +++ b/plugins/woocommerce/src/Blocks/AI/Connection.php @@ -10,6 +10,8 @@ use WpOrg\Requests\Requests; /** * Class Connection + * + * @internal */ class Connection { const TEXT_COMPLETION_API_URL = 'https://public-api.wordpress.com/wpcom/v2/text-completion'; diff --git a/plugins/woocommerce/src/Blocks/AIContent/ContentProcessor.php b/plugins/woocommerce/src/Blocks/AIContent/ContentProcessor.php index 55b202c0e3c..1f34077fb26 100644 --- a/plugins/woocommerce/src/Blocks/AIContent/ContentProcessor.php +++ b/plugins/woocommerce/src/Blocks/AIContent/ContentProcessor.php @@ -10,6 +10,8 @@ use WP_Error; * ContentProcessor class. * * Process images for content + * + * @internal */ class ContentProcessor { diff --git a/plugins/woocommerce/src/Blocks/AIContent/PatternsDictionary.php b/plugins/woocommerce/src/Blocks/AIContent/PatternsDictionary.php index cb87c48b8ea..9a881e47787 100644 --- a/plugins/woocommerce/src/Blocks/AIContent/PatternsDictionary.php +++ b/plugins/woocommerce/src/Blocks/AIContent/PatternsDictionary.php @@ -5,6 +5,8 @@ namespace Automattic\WooCommerce\Blocks\AIContent; /** * Patterns Dictionary class. + * + * @internal */ class PatternsDictionary { /** diff --git a/plugins/woocommerce/src/Blocks/AIContent/PatternsHelper.php b/plugins/woocommerce/src/Blocks/AIContent/PatternsHelper.php index d6eceadb97e..560d7ec831b 100644 --- a/plugins/woocommerce/src/Blocks/AIContent/PatternsHelper.php +++ b/plugins/woocommerce/src/Blocks/AIContent/PatternsHelper.php @@ -6,6 +6,8 @@ use WP_Error; /** * Patterns Helper class. + * + * @internal */ class PatternsHelper { /** diff --git a/plugins/woocommerce/src/Blocks/AIContent/UpdatePatterns.php b/plugins/woocommerce/src/Blocks/AIContent/UpdatePatterns.php index d9e792e85eb..d2e0228ca5f 100644 --- a/plugins/woocommerce/src/Blocks/AIContent/UpdatePatterns.php +++ b/plugins/woocommerce/src/Blocks/AIContent/UpdatePatterns.php @@ -7,6 +7,8 @@ use WP_Error; /** * Pattern Images class. + * + * @internal */ class UpdatePatterns { diff --git a/plugins/woocommerce/src/Blocks/AIContent/UpdateProducts.php b/plugins/woocommerce/src/Blocks/AIContent/UpdateProducts.php index 20e0549f8e3..571cb08029e 100644 --- a/plugins/woocommerce/src/Blocks/AIContent/UpdateProducts.php +++ b/plugins/woocommerce/src/Blocks/AIContent/UpdateProducts.php @@ -6,6 +6,8 @@ use Automattic\WooCommerce\Blocks\AI\Connection; use WP_Error; /** * Pattern Images class. + * + * @internal */ class UpdateProducts { diff --git a/plugins/woocommerce/src/Blocks/Patterns/AIPatterns.php b/plugins/woocommerce/src/Blocks/Patterns/AIPatterns.php index 8bcf093fd1f..6982fad05a7 100644 --- a/plugins/woocommerce/src/Blocks/Patterns/AIPatterns.php +++ b/plugins/woocommerce/src/Blocks/Patterns/AIPatterns.php @@ -8,6 +8,8 @@ use Automattic\WooCommerce\Blocks\Images\Pexels; /** * AIPatterns class. + * + * @internal */ class AIPatterns { const PATTERNS_AI_DATA_POST_TYPE = 'patterns_ai_data'; diff --git a/plugins/woocommerce/src/Blocks/Patterns/PTKClient.php b/plugins/woocommerce/src/Blocks/Patterns/PTKClient.php index e319c22ffed..2b86b3c857c 100644 --- a/plugins/woocommerce/src/Blocks/Patterns/PTKClient.php +++ b/plugins/woocommerce/src/Blocks/Patterns/PTKClient.php @@ -5,6 +5,8 @@ use WP_Error; /** * PatternsToolkit class. + * + * @internal */ class PTKClient { /** diff --git a/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php b/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php index 2b24e1a9657..1ce873b39f8 100644 --- a/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php +++ b/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php @@ -7,6 +7,8 @@ use WP_Upgrader; /** * PTKPatterns class. + * + * @internal */ class PTKPatternsStore { const TRANSIENT_NAME = 'ptk_patterns'; diff --git a/plugins/woocommerce/src/Blocks/Patterns/PatternRegistry.php b/plugins/woocommerce/src/Blocks/Patterns/PatternRegistry.php index 9a79a95cc5b..3af03be5200 100644 --- a/plugins/woocommerce/src/Blocks/Patterns/PatternRegistry.php +++ b/plugins/woocommerce/src/Blocks/Patterns/PatternRegistry.php @@ -5,6 +5,8 @@ use Automattic\WooCommerce\Admin\Features\Features; /** * PatternRegistry class. + * + * @internal */ class PatternRegistry { const SLUG_REGEX = '/^[A-z0-9\/_-]+$/'; From 2890e16c86eff1b8c70c64fbd6547a78a6dfc281 Mon Sep 17 00:00:00 2001 From: louwie17 Date: Fri, 30 Aug 2024 18:27:10 +0200 Subject: [PATCH 004/187] Add product data views list to experimental product data views page (#51008) * Add products data views list * Add navigation on the left * Update edit site package * Fix styling * Add changelog * Add wp/icons package to syncpack exception list * Delete some unused stuff and address types * Add changelog * Remove un needed css * Remove dependency on edit-site package * Fix custom status filters * Make sure page size works with view config * Remove use of canvasMode and navigation context as it is not needed * Remove wordpress/dom from syncpack --- .syncpackrc | 4 +- .../changelog/add-product_data_views_list | 4 + packages/js/product-editor/package.json | 3 +- .../src/products-app/constants.ts | 8 + .../product-editor/src/products-app/index.tsx | 6 +- .../src/products-app/layout.tsx | 118 ++ .../src/products-app/product-list/index.tsx | 343 +++++ .../src/products-app/router.tsx | 68 + .../sidebar-dataviews/dataview-item.tsx | 110 ++ .../sidebar-dataviews/default-views.ts | 179 +++ .../products-app/sidebar-dataviews/index.tsx | 55 + .../products-app/sidebar-dataviews/style.scss | 19 + .../sidebar-navigation-item/index.tsx | 88 ++ .../sidebar-navigation-screen/index.tsx | 136 ++ .../sidebar-button.tsx | 18 + .../src/products-app/sidebar/index.tsx | 31 + .../src/products-app/site-hub/index.tsx | 114 ++ .../src/products-app/site-hub/site-icon.tsx | 56 + packages/js/product-editor/src/products.scss | 7 +- packages/js/product-editor/typings/index.d.ts | 22 + plugins/woocommerce-admin/webpack.config.js | 6 + .../changelog/add-product_data_views_list | 4 + pnpm-lock.yaml | 1242 ++++++++--------- 23 files changed, 1988 insertions(+), 653 deletions(-) create mode 100644 packages/js/product-editor/changelog/add-product_data_views_list create mode 100644 packages/js/product-editor/src/products-app/constants.ts create mode 100644 packages/js/product-editor/src/products-app/layout.tsx create mode 100644 packages/js/product-editor/src/products-app/product-list/index.tsx create mode 100644 packages/js/product-editor/src/products-app/router.tsx create mode 100644 packages/js/product-editor/src/products-app/sidebar-dataviews/dataview-item.tsx create mode 100644 packages/js/product-editor/src/products-app/sidebar-dataviews/default-views.ts create mode 100644 packages/js/product-editor/src/products-app/sidebar-dataviews/index.tsx create mode 100644 packages/js/product-editor/src/products-app/sidebar-dataviews/style.scss create mode 100644 packages/js/product-editor/src/products-app/sidebar-navigation-item/index.tsx create mode 100644 packages/js/product-editor/src/products-app/sidebar-navigation-screen/index.tsx create mode 100644 packages/js/product-editor/src/products-app/sidebar-navigation-screen/sidebar-button.tsx create mode 100644 packages/js/product-editor/src/products-app/sidebar/index.tsx create mode 100644 packages/js/product-editor/src/products-app/site-hub/index.tsx create mode 100644 packages/js/product-editor/src/products-app/site-hub/site-icon.tsx create mode 100644 plugins/woocommerce/changelog/add-product_data_views_list diff --git a/.syncpackrc b/.syncpackrc index 7a4664d5842..ce95ab63970 100644 --- a/.syncpackrc +++ b/.syncpackrc @@ -201,7 +201,9 @@ "@wordpress/interface", "@wordpress/router", "@wordpress/edit-site", - "@wordpress/private-apis" + "@wordpress/private-apis", + "@wordpress/dataviews", + "@wordpress/icons" ], "packages": [ "@woocommerce/block-templates", diff --git a/packages/js/product-editor/changelog/add-product_data_views_list b/packages/js/product-editor/changelog/add-product_data_views_list new file mode 100644 index 00000000000..548ca3eb055 --- /dev/null +++ b/packages/js/product-editor/changelog/add-product_data_views_list @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add sidebar and dataviews list to the experimental dataviews products page. diff --git a/packages/js/product-editor/package.json b/packages/js/product-editor/package.json index ba2ec86f116..52ec6eacb88 100644 --- a/packages/js/product-editor/package.json +++ b/packages/js/product-editor/package.json @@ -56,6 +56,7 @@ "@wordpress/compose": "wp-6.0", "@wordpress/core-data": "wp-6.0", "@wordpress/data": "wp-6.0", + "@wordpress/dataviews": "^4.2.0", "@wordpress/date": "wp-6.0", "@wordpress/deprecated": "wp-6.0", "@wordpress/edit-post": "wp-6.0", @@ -64,7 +65,7 @@ "@wordpress/hooks": "wp-6.0", "@wordpress/html-entities": "wp-6.0", "@wordpress/i18n": "wp-6.0", - "@wordpress/icons": "wp-6.0", + "@wordpress/icons": "10.6.0", "@wordpress/interface": "wp-6.0", "@wordpress/keyboard-shortcuts": "wp-6.0", "@wordpress/keycodes": "wp-6.0", diff --git a/packages/js/product-editor/src/products-app/constants.ts b/packages/js/product-editor/src/products-app/constants.ts new file mode 100644 index 00000000000..8c2b1b338f9 --- /dev/null +++ b/packages/js/product-editor/src/products-app/constants.ts @@ -0,0 +1,8 @@ +export const LAYOUT_GRID = 'grid'; +export const LAYOUT_TABLE = 'table'; +export const LAYOUT_LIST = 'list'; + +export const OPERATOR_IS = 'is'; +export const OPERATOR_IS_NOT = 'isNot'; +export const OPERATOR_IS_ANY = 'isAny'; +export const OPERATOR_IS_NONE = 'isNone'; diff --git a/packages/js/product-editor/src/products-app/index.tsx b/packages/js/product-editor/src/products-app/index.tsx index 69bef7fb2af..44c15e41dfa 100644 --- a/packages/js/product-editor/src/products-app/index.tsx +++ b/packages/js/product-editor/src/products-app/index.tsx @@ -13,12 +13,16 @@ import { * Internal dependencies */ import { unlock } from '../lock-unlock'; +import useLayoutAreas from './router'; +import { Layout } from './layout'; const { RouterProvider } = unlock( routerPrivateApis ); const { GlobalStylesProvider } = unlock( editorPrivateApis ); function ProductsLayout() { - return
Initial Products Layout
; + // This ensures the edited entity id and type are initialized properly. + const route = useLayoutAreas(); + return ; } export function ProductsApp() { diff --git a/packages/js/product-editor/src/products-app/layout.tsx b/packages/js/product-editor/src/products-app/layout.tsx new file mode 100644 index 00000000000..fe084ebf646 --- /dev/null +++ b/packages/js/product-editor/src/products-app/layout.tsx @@ -0,0 +1,118 @@ +/** + * External dependencies + */ +import { createElement, Fragment, useRef } from '@wordpress/element'; +import { + useViewportMatch, + useResizeObserver, + useReducedMotion, +} from '@wordpress/compose'; +import { __ } from '@wordpress/i18n'; +import { + // @ts-expect-error missing type. + EditorSnackbars, + // @ts-expect-error missing type. + privateApis as editorPrivateApis, +} from '@wordpress/editor'; +// eslint-disable-next-line @woocommerce/dependency-group +import { + // @ts-expect-error missing type. + __unstableMotion as motion, + // @ts-expect-error missing type. + __unstableAnimatePresence as AnimatePresence, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import SidebarContent from './sidebar'; +import SiteHub from './site-hub'; +import { Route } from './router'; +import { unlock } from '../lock-unlock'; + +const { NavigableRegion } = unlock( editorPrivateApis ); + +const ANIMATION_DURATION = 0.3; + +type LayoutProps = { + route: Route; +}; + +export function Layout( { route }: LayoutProps ) { + const [ fullResizer ] = useResizeObserver(); + const toggleRef = useRef< HTMLAnchorElement >( null ); + const isMobileViewport = useViewportMatch( 'medium', '<' ); + const disableMotion = useReducedMotion(); + + const { key: routeKey, areas, widths } = route; + + return ( + <> + { fullResizer } +
+
+ { /* + The NavigableRegion must always be rendered and not use + `inert` otherwise `useNavigateRegions` will fail. + */ } + { ( ! isMobileViewport || ! areas.mobile ) && ( + + + + + + { areas.sidebar } + + + + + ) } + + + + { ! isMobileViewport && areas.content && ( +
+ { areas.content } +
+ ) } + + { ! isMobileViewport && areas.edit && ( +
+ { areas.edit } +
+ ) } +
+
+ + ); +} diff --git a/packages/js/product-editor/src/products-app/product-list/index.tsx b/packages/js/product-editor/src/products-app/product-list/index.tsx new file mode 100644 index 00000000000..6a0988b2448 --- /dev/null +++ b/packages/js/product-editor/src/products-app/product-list/index.tsx @@ -0,0 +1,343 @@ +/** + * External dependencies + */ +import { Action, DataViews, View } from '@wordpress/dataviews'; +import { + createElement, + useState, + useMemo, + useCallback, + useEffect, +} from '@wordpress/element'; +import { Product, ProductQuery } from '@woocommerce/data'; +import { drawerRight } from '@wordpress/icons'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { __ } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; +import classNames from 'classnames'; +import { + // @ts-expect-error missing types. + __experimentalHeading as Heading, + // @ts-expect-error missing types. + __experimentalText as Text, + // @ts-expect-error missing types. + __experimentalHStack as HStack, + // @ts-expect-error missing types. + __experimentalVStack as VStack, + FlexItem, + Button, +} from '@wordpress/components'; +// @ts-expect-error missing type. +// eslint-disable-next-line @woocommerce/dependency-group +import { privateApis as editorPrivateApis } from '@wordpress/editor'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; +import { + useDefaultViews, + defaultLayouts, +} from '../sidebar-dataviews/default-views'; +import { LAYOUT_LIST, OPERATOR_IS } from '../constants'; + +const { NavigableRegion } = unlock( editorPrivateApis ); +const { useHistory, useLocation } = unlock( routerPrivateApis ); + +const STATUSES = [ + { value: 'draft', label: __( 'Draft', 'woocommerce' ) }, + { value: 'future', label: __( 'Scheduled', 'woocommerce' ) }, + { value: 'private', label: __( 'Private', 'woocommerce' ) }, + { value: 'publish', label: __( 'Published', 'woocommerce' ) }, + { value: 'trash', label: __( 'Trash', 'woocommerce' ) }, +]; + +/** + * TODO: auto convert some of the product editor blocks ( from the blocks directory ) to this format. + * The edit function should work relatively well with the edit from the blocks, the only difference is that the blocks rely on getEntityProp to get the value + */ +const fields = [ + { + id: 'name', + label: __( 'Name', 'woocommerce' ), + enableHiding: false, + type: 'text', + render: function nameRender( { item }: { item: Product } ) { + return item.name; + }, + }, + { + id: 'sku', + label: __( 'SKU', 'woocommerce' ), + enableHiding: false, + enableSorting: false, + render: ( { item }: { item: Product } ) => { + return item.sku; + }, + }, + { + id: 'date', + label: __( 'Date', 'woocommerce' ), + render: ( { item }: { item: Product } ) => { + return ; + }, + }, + { + label: __( 'Status', 'woocommerce' ), + id: 'status', + getValue: ( { item }: { item: Product } ) => + STATUSES.find( ( { value } ) => value === item.status )?.label ?? + item.status, + elements: STATUSES, + filterBy: { + operators: [ OPERATOR_IS ], + }, + enableSorting: false, + }, +]; + +export type ProductListProps = { + subTitle?: string; + className?: string; + hideTitleFromUI?: boolean; + postType?: string; +}; + +const PAGE_SIZE = 25; +const EMPTY_ARRAY: Product[] = []; +const EMPTY_ACTIONS_ARRAY: Action< Product >[] = []; + +const getDefaultView = ( + defaultViews: Array< { slug: string; view: View } >, + activeView: string +) => { + return defaultViews.find( ( { slug } ) => slug === activeView )?.view; +}; + +/** + * This function abstracts working with default & custom views by + * providing a [ state, setState ] tuple based on the URL parameters. + * + * Consumers use the provided tuple to work with state + * and don't have to deal with the specifics of default & custom views. + * + * @param {string} postType Post type to retrieve default views for. + * @return {Array} The [ state, setState ] tuple. + */ +function useView( + postType: string +): [ View, ( view: View ) => void, ( view: View ) => void ] { + const { + params: { activeView = 'all', isCustom = 'false', layout }, + } = useLocation(); + const history = useHistory(); + + const defaultViews = useDefaultViews( { postType } ); + const [ view, setView ] = useState< View >( () => { + const initialView = getDefaultView( defaultViews, activeView ) ?? { + type: layout ?? LAYOUT_LIST, + }; + + const type = layout ?? initialView.type; + return { + ...initialView, + type, + }; + } ); + + const setViewWithUrlUpdate = useCallback( + ( newView: View ) => { + const { params } = history.getLocationWithParams(); + + if ( newView.type === LAYOUT_LIST && ! params?.layout ) { + // Skip updating the layout URL param if + // it is not present and the newView.type is LAYOUT_LIST. + } else if ( newView.type !== params?.layout ) { + history.push( { + ...params, + layout: newView.type, + } ); + } + + setView( newView ); + }, + [ history, isCustom ] + ); + + // When layout URL param changes, update the view type + // without affecting any other config. + useEffect( () => { + setView( ( prevView ) => ( { + ...prevView, + type: layout ?? LAYOUT_LIST, + } ) ); + }, [ layout ] ); + + // When activeView or isCustom URL parameters change, reset the view. + useEffect( () => { + const newView = getDefaultView( defaultViews, activeView ); + + if ( newView ) { + const type = layout ?? newView.type; + setView( { + ...newView, + type, + } ); + } + }, [ activeView, isCustom, layout, defaultViews ] ); + + return [ view, setViewWithUrlUpdate, setViewWithUrlUpdate ]; +} + +function getItemId( item: Product ) { + return item.id.toString(); +} + +export default function ProductList( { + subTitle, + className, + hideTitleFromUI = false, +}: ProductListProps ) { + const history = useHistory(); + const location = useLocation(); + const { + postId, + quickEdit = false, + postType = 'product', + isCustom, + activeView = 'all', + } = location.params; + const [ selection, setSelection ] = useState( [ postId ] ); + const [ view, setView ] = useView( postType ); + + const queryParams = useMemo( () => { + const filters: Partial< ProductQuery > = {}; + view.filters?.forEach( ( filter ) => { + if ( filter.field === 'status' ) { + filters.status = Array.isArray( filter.value ) + ? filter.value.join( ',' ) + : filter.value; + } + } ); + const orderby = + view.sort?.field === 'name' ? 'title' : view.sort?.field; + + return { + per_page: view.perPage, + page: view.page, + order: view.sort?.direction, + orderby, + search: view.search, + ...filters, + }; + }, [ location.params, view ] ); + + const onChangeSelection = useCallback( + ( items ) => { + setSelection( items ); + history.push( { + ...location.params, + postId: items.join( ',' ), + } ); + }, + [ history, location.params, view?.type ] + ); + + // TODO: Use the Woo data store to get all the products, as this doesn't contain all the product data. + const { records, totalCount, isLoading } = useSelect( + ( select ) => { + const { getProducts, getProductsTotalCount, isResolving } = + select( 'wc/admin/products' ); + return { + records: getProducts( queryParams ) as Product[], + totalCount: getProductsTotalCount( queryParams ) as number, + isLoading: isResolving( 'getProducts', [ queryParams ] ), + }; + }, + [ queryParams ] + ); + + const paginationInfo = useMemo( + () => ( { + totalItems: totalCount, + totalPages: Math.ceil( totalCount / ( view.perPage || PAGE_SIZE ) ), + } ), + [ totalCount, view.perPage ] + ); + + const classes = classNames( 'edit-site-page', className ); + + return ( + +
+ { ! hideTitleFromUI && ( + + + + { __( 'Products', 'woocommerce' ) } + + + { /* { actions } */ } + + + { subTitle && ( + + { subTitle } + + ) } + + ) } + { + history.push( { + ...location.params, + quickEdit: quickEdit ? undefined : true, + } ); + } } + /> + } + /> +
+
+ ); +} diff --git a/packages/js/product-editor/src/products-app/router.tsx b/packages/js/product-editor/src/products-app/router.tsx new file mode 100644 index 00000000000..3acbc510aba --- /dev/null +++ b/packages/js/product-editor/src/products-app/router.tsx @@ -0,0 +1,68 @@ +/** + * External dependencies + */ +import { createElement } from '@wordpress/element'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; + +/** + * Internal dependencies + */ +import { unlock } from '../lock-unlock'; +import ProductList from './product-list'; +import DataViewsSidebarContent from './sidebar-dataviews'; +import SidebarNavigationScreen from './sidebar-navigation-screen'; + +const { useLocation } = unlock( routerPrivateApis ); + +export type Route = { + key: string; + areas: { + sidebar: React.JSX.Element | React.FunctionComponent; + content?: React.JSX.Element | React.FunctionComponent; + edit?: React.JSX.Element | React.FunctionComponent; + mobile?: React.JSX.Element | React.FunctionComponent | boolean; + preview?: boolean; + }; + widths?: { + content?: number; + edit?: number; + sidebar?: number; + }; +}; + +export default function useLayoutAreas() { + const { params = {} } = useLocation(); + const { postType = 'product', layout = 'table', canvas } = params; + // Products list. + if ( [ 'product' ].includes( postType ) ) { + const isListLayout = layout === 'list' || ! layout; + return { + key: 'products-list', + areas: { + sidebar: ( + } + /> + ), + content: , + preview: false, + mobile: , + }, + widths: { + content: isListLayout ? 380 : undefined, + }, + }; + } + + // Fallback shows the home page preview + return { + key: 'default', + areas: { + sidebar: () => null, + preview: false, + mobile: canvas === 'edit', + }, + }; +} diff --git a/packages/js/product-editor/src/products-app/sidebar-dataviews/dataview-item.tsx b/packages/js/product-editor/src/products-app/sidebar-dataviews/dataview-item.tsx new file mode 100644 index 00000000000..03a4f8cee85 --- /dev/null +++ b/packages/js/product-editor/src/products-app/sidebar-dataviews/dataview-item.tsx @@ -0,0 +1,110 @@ +/** + * External dependencies + */ +import { createElement } from '@wordpress/element'; +import classNames from 'classnames'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { addQueryArgs, getQueryArgs, removeQueryArgs } from '@wordpress/url'; +import { VIEW_LAYOUTS } from '@wordpress/dataviews'; +// @ts-expect-error missing type. +// eslint-disable-next-line @woocommerce/dependency-group +import { __experimentalHStack as HStack } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import SidebarNavigationItem from '../sidebar-navigation-item'; +import { unlock } from '../../lock-unlock'; + +const { useHistory, useLocation } = unlock( routerPrivateApis ); + +type DataViewItemProps = { + title: string; + slug: string; + customViewId?: string; + type: string; + icon: React.JSX.Element; + isActive: boolean; + isCustom: boolean; + suffix?: string; +}; + +function useLink( + params: Record< string, string | undefined >, + state?: Record< string, string | undefined >, + shouldReplace = false +) { + const history = useHistory(); + function onClick( event: Event ) { + event?.preventDefault(); + + if ( shouldReplace ) { + history.replace( params, state ); + } else { + history.push( params, state ); + } + } + + const currentArgs = getQueryArgs( window.location.href ); + const currentUrlWithoutArgs = removeQueryArgs( + window.location.href, + ...Object.keys( currentArgs ) + ); + + const newUrl = addQueryArgs( currentUrlWithoutArgs, params ); + + return { + href: newUrl, + onClick, + }; +} + +export default function DataViewItem( { + title, + slug, + customViewId, + type, + icon, + isActive, + isCustom, + suffix, +}: DataViewItemProps ) { + const { + params: { postType, page }, + } = useLocation(); + + const iconToUse = + icon || VIEW_LAYOUTS.find( ( v ) => v.type === type )?.icon; + + let activeView: undefined | string = isCustom ? customViewId : slug; + if ( activeView === 'all' ) { + activeView = undefined; + } + const linkInfo = useLink( { + page, + postType, + layout: type, + activeView, + isCustom: isCustom ? 'true' : undefined, + } ); + return ( + + + { title } + + { suffix } + + ); +} diff --git a/packages/js/product-editor/src/products-app/sidebar-dataviews/default-views.ts b/packages/js/product-editor/src/products-app/sidebar-dataviews/default-views.ts new file mode 100644 index 00000000000..816477363f3 --- /dev/null +++ b/packages/js/product-editor/src/products-app/sidebar-dataviews/default-views.ts @@ -0,0 +1,179 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { useMemo } from '@wordpress/element'; +import { + trash, + pages, + drafts, + published, + scheduled, + notAllowed, +} from '@wordpress/icons'; +import type { ColumnStyle, ViewTable } from '@wordpress/dataviews'; + +/** + * Internal dependencies + */ +import { + LAYOUT_LIST, + LAYOUT_TABLE, + LAYOUT_GRID, + OPERATOR_IS, +} from '../constants'; + +export const defaultLayouts: Record< + string, + { + layout: { + primaryField: string; + mediaField?: string; + styles?: Record< string, ColumnStyle >; + }; + } +> = { + [ LAYOUT_TABLE ]: { + layout: { + primaryField: 'name', + styles: { + name: { + maxWidth: 300, + }, + }, + }, + }, + [ LAYOUT_GRID ]: { + layout: { + mediaField: 'featured-image', + primaryField: 'name', + }, + }, + [ LAYOUT_LIST ]: { + layout: { + primaryField: 'name', + mediaField: 'featured-image', + }, + }, +}; + +const DEFAULT_POST_BASE: Omit< ViewTable, 'view' | 'title' | 'slug' | 'icon' > = + { + type: LAYOUT_TABLE, + search: '', + filters: [], + page: 1, + perPage: 20, + sort: { + field: 'date', + direction: 'desc', + }, + fields: [ 'name', 'sku', 'status', 'date' ], + layout: defaultLayouts[ LAYOUT_LIST ].layout, + }; + +export function useDefaultViews( { postType }: { postType: string } ): Array< { + title: string; + slug: string; + icon: React.JSX.Element; + view: ViewTable; +} > { + const labels = useSelect( + ( select ) => { + const { getPostType } = select( coreStore ); + const postTypeData: { labels?: Record< string, string > } = + getPostType( postType ); + return postTypeData?.labels; + }, + [ postType ] + ); + return useMemo( () => { + return [ + { + title: labels?.all_items || __( 'All items', 'woocommerce' ), + slug: 'all', + icon: pages, + view: { ...DEFAULT_POST_BASE }, + }, + { + title: __( 'Published', 'woocommerce' ), + slug: 'published', + icon: published, + view: { + ...DEFAULT_POST_BASE, + filters: [ + { + field: 'status', + operator: OPERATOR_IS, + value: 'publish', + }, + ], + }, + }, + { + title: __( 'Scheduled', 'woocommerce' ), + slug: 'future', + icon: scheduled, + view: { + ...DEFAULT_POST_BASE, + filters: [ + { + field: 'status', + operator: OPERATOR_IS, + value: 'future', + }, + ], + }, + }, + { + title: __( 'Drafts', 'woocommerce' ), + slug: 'drafts', + icon: drafts, + view: { + ...DEFAULT_POST_BASE, + filters: [ + { + field: 'status', + operator: OPERATOR_IS, + value: 'draft', + }, + ], + }, + }, + { + title: __( 'Private', 'woocommerce' ), + slug: 'private', + icon: notAllowed, + view: { + ...DEFAULT_POST_BASE, + filters: [ + { + field: 'status', + operator: OPERATOR_IS, + value: 'private', + }, + ], + }, + }, + { + title: __( 'Trash', 'woocommerce' ), + slug: 'trash', + icon: trash, + view: { + ...DEFAULT_POST_BASE, + type: LAYOUT_TABLE, + layout: defaultLayouts[ LAYOUT_TABLE ].layout, + filters: [ + { + field: 'status', + operator: OPERATOR_IS, + value: 'trash', + }, + ], + }, + }, + ]; + }, [ labels ] ); +} diff --git a/packages/js/product-editor/src/products-app/sidebar-dataviews/index.tsx b/packages/js/product-editor/src/products-app/sidebar-dataviews/index.tsx new file mode 100644 index 00000000000..245675bfcf3 --- /dev/null +++ b/packages/js/product-editor/src/products-app/sidebar-dataviews/index.tsx @@ -0,0 +1,55 @@ +/** + * External dependencies + */ +import { createElement, Fragment } from '@wordpress/element'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; +// @ts-expect-error missing type. +// eslint-disable-next-line @wordpress/no-unsafe-wp-apis, @woocommerce/dependency-group +import { __experimentalItemGroup as ItemGroup } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; +import DataViewItem from './dataview-item'; +import { useDefaultViews } from './default-views'; + +const { useLocation } = unlock( routerPrivateApis ); + +export default function DataViewsSidebarContent() { + const { + params: { + postType = 'product', + activeView = 'all', + isCustom = 'false', + }, + } = useLocation(); + const defaultViews = useDefaultViews( { postType } ); + if ( ! postType ) { + return null; + } + const isCustomBoolean = isCustom === 'true'; + + return ( + <> + + { defaultViews.map( ( dataview ) => { + return ( + + ); + } ) } + + + ); +} diff --git a/packages/js/product-editor/src/products-app/sidebar-dataviews/style.scss b/packages/js/product-editor/src/products-app/sidebar-dataviews/style.scss new file mode 100644 index 00000000000..d3744634f6a --- /dev/null +++ b/packages/js/product-editor/src/products-app/sidebar-dataviews/style.scss @@ -0,0 +1,19 @@ +.edit-site-sidebar-navigation-screen__title-icon { + position: sticky; + top: 0; + background: $gray-900; + padding-top: $grid-unit-60; + margin-bottom: $grid-unit-10; + padding-bottom: $grid-unit-10; +} + +.edit-site-sidebar-button { + color: #e0e0e0; + flex-shrink: 0; +} + +.edit-site-sidebar-navigation-screen__title { + flex-grow: 1; + overflow-wrap: break-word; + padding: 2px 0 0 +} diff --git a/packages/js/product-editor/src/products-app/sidebar-navigation-item/index.tsx b/packages/js/product-editor/src/products-app/sidebar-navigation-item/index.tsx new file mode 100644 index 00000000000..57f8c23657c --- /dev/null +++ b/packages/js/product-editor/src/products-app/sidebar-navigation-item/index.tsx @@ -0,0 +1,88 @@ +/** + * External dependencies + */ +import { isRTL } from '@wordpress/i18n'; +import { chevronRightSmall, chevronLeftSmall, Icon } from '@wordpress/icons'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; +import classNames from 'classnames'; +import { createElement } from '@wordpress/element'; +import { + // @ts-expect-error missing type. + __experimentalItem as Item, + // @ts-expect-error missing type. + __experimentalHStack as HStack, + FlexBlock, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; + +const { useHistory } = unlock( routerPrivateApis ); + +type SidebarNavigationItemProps = { + className?: string; + icon?: React.JSX.Element; + suffix?: string; + withChevron?: boolean; + uid?: string; + params?: Record< string, string >; + onClick?: ( e: Event ) => void; + children: React.ReactNode; +}; + +export default function SidebarNavigationItem( { + className, + icon, + withChevron = false, + suffix, + uid, + params, + onClick, + children, + ...props +}: SidebarNavigationItemProps ) { + const history = useHistory(); + // If there is no custom click handler, create one that navigates to `params`. + function handleClick( e: Event ) { + if ( onClick ) { + onClick( e ); + } else if ( params ) { + e.preventDefault(); + history.push( params ); + } + } + + return ( + + + { icon && ( + + ) } + { children } + { withChevron && ( + + ) } + { ! withChevron && suffix } + + + ); +} diff --git a/packages/js/product-editor/src/products-app/sidebar-navigation-screen/index.tsx b/packages/js/product-editor/src/products-app/sidebar-navigation-screen/index.tsx new file mode 100644 index 00000000000..03a669c3ea2 --- /dev/null +++ b/packages/js/product-editor/src/products-app/sidebar-navigation-screen/index.tsx @@ -0,0 +1,136 @@ +/** + * External dependencies + */ + +import { isRTL, __ } from '@wordpress/i18n'; +import classNames from 'classnames'; +import { chevronRight, chevronLeft } from '@wordpress/icons'; +import { useSelect } from '@wordpress/data'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { createElement, Fragment } from '@wordpress/element'; +import { + // @ts-expect-error missing type. + __experimentalHStack as HStack, + // @ts-expect-error missing type. + __experimentalHeading as Heading, + // @ts-expect-error missing type. + __experimentalVStack as VStack, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; +import SidebarButton from './sidebar-button'; + +const { useHistory, useLocation } = unlock( routerPrivateApis ); + +type SidebarNavigationScreenProps = { + isRoot?: boolean; + title: string; + actions?: React.JSX.Element; + meta?: string; + content: React.JSX.Element; + footer?: string; + description?: string; + backPath?: string; +}; + +export default function SidebarNavigationScreen( { + isRoot, + title, + actions, + meta, + content, + footer, + description, + backPath: backPathProp, +}: SidebarNavigationScreenProps ) { + const { dashboardLink, dashboardLinkText } = useSelect( ( select ) => { + const { getSettings } = unlock( select( 'core/edit-site' ) ); + return { + dashboardLink: getSettings().__experimentalDashboardLink, + dashboardLinkText: getSettings().__experimentalDashboardLinkText, + }; + }, [] ); + const location = useLocation(); + const history = useHistory(); + const backPath = backPathProp ?? location.state?.backPath; + const icon = isRTL() ? chevronRight : chevronLeft; + + return ( + <> + + + { ! isRoot && ( + { + history.push( backPath ); + } } + icon={ icon } + label={ __( 'Back', 'woocommerce' ) } + showTooltip={ false } + /> + ) } + { isRoot && ( + + ) } + + { title } + + { actions && ( +
+ { actions } +
+ ) } +
+ { meta && ( + <> +
+ { meta } +
+ + ) } + +
+ { description && ( +

+ { description } +

+ ) } + { content } +
+
+ { footer && ( +
+ { footer } +
+ ) } + + ); +} diff --git a/packages/js/product-editor/src/products-app/sidebar-navigation-screen/sidebar-button.tsx b/packages/js/product-editor/src/products-app/sidebar-navigation-screen/sidebar-button.tsx new file mode 100644 index 00000000000..6033e202bf6 --- /dev/null +++ b/packages/js/product-editor/src/products-app/sidebar-navigation-screen/sidebar-button.tsx @@ -0,0 +1,18 @@ +/** + * External dependencies + */ +import { createElement } from '@wordpress/element'; +import { Button } from '@wordpress/components'; +import classNames from 'classnames'; + +export default function SidebarButton( props: Button.Props ) { + return ( + + + + +
+ +
+
+ + + ); + } + ) +); + +export default SiteHub; diff --git a/packages/js/product-editor/src/products-app/site-hub/site-icon.tsx b/packages/js/product-editor/src/products-app/site-hub/site-icon.tsx new file mode 100644 index 00000000000..754dd6e39fc --- /dev/null +++ b/packages/js/product-editor/src/products-app/site-hub/site-icon.tsx @@ -0,0 +1,56 @@ +/** + * External dependencies + */ +import { createElement } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { Icon } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { wordpress } from '@wordpress/icons'; +import { store as coreDataStore } from '@wordpress/core-data'; +import classNames from 'classnames'; + +type SiteIconProps = { + className: string; +}; + +function SiteIcon( { className }: SiteIconProps ) { + const { isRequestingSite, siteIconUrl } = useSelect( ( select ) => { + const { getEntityRecord } = select( coreDataStore ); + const siteData: { site_icon_url?: string } = getEntityRecord( + 'root', + '__unstableBase', + undefined + ); + + return { + isRequestingSite: ! siteData, + siteIconUrl: siteData?.site_icon_url, + }; + }, [] ); + + if ( isRequestingSite && ! siteIconUrl ) { + return
; + } + + const icon = siteIconUrl ? ( + { + ) : ( + + ); + + return ( +
+ { icon } +
+ ); +} + +export default SiteIcon; diff --git a/packages/js/product-editor/src/products.scss b/packages/js/product-editor/src/products.scss index 9a1199c1c33..7aaa6b290a4 100644 --- a/packages/js/product-editor/src/products.scss +++ b/packages/js/product-editor/src/products.scss @@ -4,11 +4,12 @@ .woocommerce_page_woocommerce-products-dashboard #adminmenumain { display: none; } + .woocommerce_page_woocommerce-products-dashboard #wpcontent { margin-left: 0; } -body.woocommerce_page_woocommerce-products-dashboard - #woocommerce-products-dashboard { + +body.woocommerce_page_woocommerce-products-dashboard #woocommerce-products-dashboard { @include wp-admin-reset("#woocommerce-products-dashboard"); @include reset; display: block !important; @@ -38,3 +39,5 @@ body.js.is-fullscreen-mode { } } } + +@import "products-app/sidebar-dataviews/style.scss"; diff --git a/packages/js/product-editor/typings/index.d.ts b/packages/js/product-editor/typings/index.d.ts index 92aeddf3d87..1fb7d394b5b 100644 --- a/packages/js/product-editor/typings/index.d.ts +++ b/packages/js/product-editor/typings/index.d.ts @@ -31,6 +31,7 @@ declare module '@wordpress/core-data' { isResolving: boolean; hasResolved: boolean; }; + const store: string; } declare module '@wordpress/keyboard-shortcuts' { function useShortcut( @@ -43,3 +44,24 @@ declare module '@wordpress/keyboard-shortcuts' { declare module '@wordpress/router' { const privateApis; } + +declare module '@wordpress/edit-site/build-module/components/sync-state-with-url/use-init-edited-entity-from-url' { + export default function useInitEditedEntityFromURL(): void; +} + +declare module '@wordpress/edit-site/build-module/components/sidebar-navigation-screen' { + const SidebarNavigationScreen: React.FunctionComponent< { + title: string; + isRoot: boolean; + content: JSX.Element; + } >; + export default SidebarNavigationScreen; +} + +declare module '@wordpress/edit-site/build-module/components/site-hub' { + const SiteHub: React.FunctionComponent< { + ref: React.Ref; + isTransparent: boolean; + } >; + export default SiteHub; +} diff --git a/plugins/woocommerce-admin/webpack.config.js b/plugins/woocommerce-admin/webpack.config.js index cadd1251831..469d48e3f3a 100644 --- a/plugins/woocommerce-admin/webpack.config.js +++ b/plugins/woocommerce-admin/webpack.config.js @@ -186,6 +186,8 @@ const webpackConfig = { extensions: [ '.json', '.js', '.jsx', '.ts', '.tsx' ], alias: { '~': path.resolve( __dirname + '/client' ), + 'react/jsx-dev-runtime': require.resolve( 'react/jsx-dev-runtime' ), + 'react/jsx-runtime': require.resolve( 'react/jsx-runtime' ), }, }, plugins: [ @@ -240,6 +242,10 @@ const webpackConfig = { return null; } + if ( request.startsWith( '@wordpress/dataviews' ) ) { + return null; + } + if ( request.startsWith( '@wordpress/edit-site' ) ) { // The external wp.editSite does not include edit-site components, so we need to skip requesting to external here. We can remove this once the edit-site components are exported in the external wp.editSite. // We use the edit-site components in the customize store. diff --git a/plugins/woocommerce/changelog/add-product_data_views_list b/plugins/woocommerce/changelog/add-product_data_views_list new file mode 100644 index 00000000000..7669d680be8 --- /dev/null +++ b/plugins/woocommerce/changelog/add-product_data_views_list @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update webpack config to bundle in @wordpress/dataviews package. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db28d78d96a..ec49d327648 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.24.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(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) + version: 29.1.1(@babel/core@7.24.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(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) 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@9.4.0) + version: 4.3.4(supports-color@8.1.1) 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.24.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(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) + version: 29.1.1(@babel/core@7.24.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(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) typescript: specifier: ^5.3.3 version: 5.3.3 @@ -2616,6 +2616,9 @@ importers: '@wordpress/data': 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) '@wordpress/date': specifier: wp-6.0 version: 4.6.1 @@ -2641,8 +2644,8 @@ importers: specifier: wp-6.0 version: 4.6.1 '@wordpress/icons': - specifier: wp-6.0 - version: 8.2.3 + specifier: 10.6.0 + version: 10.6.0(react@17.0.2) '@wordpress/interface': specifier: wp-6.0 version: 4.5.6(@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) @@ -2878,7 +2881,7 @@ importers: version: 5.0.5 ts-jest: specifier: ~29.1.1 - version: 29.1.1(@babel/core@7.24.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(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) + version: 29.1.1(@babel/core@7.24.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(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) typescript: specifier: ^5.3.3 version: 5.3.3 @@ -2890,7 +2893,7 @@ importers: dependencies: debug: specifier: ^4.3.4 - version: 4.3.4(supports-color@9.4.0) + version: 4.3.4(supports-color@8.1.1) devDependencies: '@babel/core': specifier: ^7.23.5 @@ -2999,7 +3002,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@9.4.0) + version: 4.3.4(supports-color@8.1.1) prop-types: specifier: ^15.8.1 version: 15.8.1 @@ -3376,7 +3379,7 @@ importers: version: 3.34.0 debug: specifier: ^4.3.4 - version: 4.3.4(supports-color@9.4.0) + version: 4.3.4(supports-color@8.1.1) dompurify: specifier: ^2.4.7 version: 2.4.7 @@ -4666,7 +4669,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.24.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(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) + version: 29.1.1(@babel/core@7.24.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(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-node: specifier: ^10.9.2 version: 10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3) @@ -4851,7 +4854,7 @@ importers: version: 1.2.2 ts-jest: specifier: ~29.1.1 - version: 29.1.1(@babel/core@7.24.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(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) + version: 29.1.1(@babel/core@7.24.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(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-loader: specifier: ^9.5.1 version: 9.5.1(typescript@5.3.3)(webpack@5.89.0(webpack-cli@3.3.12)) @@ -5105,23 +5108,23 @@ packages: '@ariakit/core@0.3.11': resolution: {integrity: sha512-+MnOeqnA4FLI/7vqsZLbZQHHN4ofd9kvkNjz44fNi0gqmD+ZbMWiDkFAvZII75dYnxYw5ZPpWjA4waK22VBWig==} - '@ariakit/core@0.3.8': - resolution: {integrity: sha512-LlSCwbyyozMX4ZEobpYGcv1LFqOdBTdTYPZw3lAVgLcFSNivsazi3NkKM9qNWNIu00MS+xTa0+RuIcuWAjlB2Q==} - '@ariakit/core@0.4.5': resolution: {integrity: sha512-e294+bEcyzt/H/kO4fS5/czLAlkF7PY+Kul3q2z54VY+GGay8NlVs9UezAB7L4jUBlYRAXwp7/1Sq3R7b+MZ7w==} + '@ariakit/core@0.4.9': + resolution: {integrity: sha512-nV0B/OTK/0iB+P9RC7fudznYZ8eR6rR1F912Zc54e3+wSW5RrRvNOiRxyMrgENidd4R7cCMDw77XJLSBLKgEPQ==} + '@ariakit/react-core@0.3.14': resolution: {integrity: sha512-16Qj6kDPglpdWtU5roY9q+G66naOjauTY5HvUIaL2aLY0187ATaRrABIKoMMzTtJyhvsud4jFlzivz+/zCQ8yw==} peerDependencies: react: ^17.0.0 || ^18.0.0 react-dom: ^17.0.0 || ^18.0.0 - '@ariakit/react-core@0.3.9': - resolution: {integrity: sha512-K1Rcxr6FpF0n3L7Uvo+e5hm+zqoZmXLRcYF/skI+/j+ole+uNbnsnfGhG1avqJlklqH4bmkFkjZzmMdOnUC0Ig==} + '@ariakit/react-core@0.4.10': + resolution: {integrity: sha512-r6DZmtHBmSoOj848+RpBwdZy/55YxPhMhfH14JIO2OLn1F6iSFkQwR7AAGpIrlYycWJFSF7KrQu50O+SSfFJdQ==} peerDependencies: - react: ^17.0.0 || ^18.0.0 - react-dom: ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 '@ariakit/react-core@0.4.5': resolution: {integrity: sha512-ciTYPwpj/+mdA+EstveEnoygbx5e4PXQJxfkLKy4lkTkDJJUS9GcbYhdnIFJVUta6P1YFvzkIKo+/y9mcbMKJg==} @@ -5135,11 +5138,11 @@ packages: react: ^17.0.0 || ^18.0.0 react-dom: ^17.0.0 || ^18.0.0 - '@ariakit/react@0.3.9': - resolution: {integrity: sha512-gC+gibh2go8wvBqzYXavlHKwAfmee5GUMrPSQ9WBBLIfm9nQElujxcHJydaRx+ULR5FbOnbZVC3vU2ic8hSrNw==} + '@ariakit/react@0.4.10': + resolution: {integrity: sha512-c1+6sNLj57aAXrBZMCVGG+OXeFrPAG0TV1jT7oPJcN/KLRs3aCuO3CCJVep/eKepFzzK01kNRGYX3wPT1TXPNw==} peerDependencies: - react: ^17.0.0 || ^18.0.0 - react-dom: ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 '@ariakit/react@0.4.5': resolution: {integrity: sha512-GUHxaOY1JZrJUHkuV20IY4NWcgknhqTQM0qCQcVZDCi+pJiWchUjTG+UyIr/Of02hU569qnQ7yovskCf+V3tNg==} @@ -6918,18 +6921,12 @@ packages: '@floating-ui/core@0.6.2': resolution: {integrity: sha512-jktYRmZwmau63adUG3GKOAVCofBXkk55S/zQ94XOorAHhwqFIOFAy1rSp2N0Wp6/tGbe9V3u/ExlGZypyY17rg==} - '@floating-ui/core@0.7.3': - resolution: {integrity: sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==} - '@floating-ui/core@1.5.2': resolution: {integrity: sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A==} '@floating-ui/dom@0.4.5': resolution: {integrity: sha512-b+prvQgJt8pieaKYMSJBXHxX/DYwdLsAWxKYqnO5dO2V4oo/TYBZJAUQCVNjTWWsrs6o4VDrNcP9+E70HAhJdw==} - '@floating-ui/dom@0.5.4': - resolution: {integrity: sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==} - '@floating-ui/dom@1.5.3': resolution: {integrity: sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==} @@ -6939,12 +6936,6 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/react-dom@0.7.2': - resolution: {integrity: sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - '@floating-ui/react-dom@1.3.0': resolution: {integrity: sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==} peerDependencies: @@ -7800,12 +7791,6 @@ packages: '@radix-ui/primitive@1.0.1': resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} - '@radix-ui/react-arrow@1.0.2': - resolution: {integrity: sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-arrow@1.0.3': resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} peerDependencies: @@ -7819,12 +7804,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-collection@1.0.2': - resolution: {integrity: sha512-s8WdQQ6wNXpaxdZ308KSr8fEWGrg4un8i4r/w7fhiS4ElRNjk5rRcl0/C6TANG2LvLOGIxtzo/jAg6Qf73TEBw==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-collection@1.0.3': resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} peerDependencies: @@ -7872,11 +7851,6 @@ packages: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-direction@1.0.0': - resolution: {integrity: sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-direction@1.0.1': resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==} peerDependencies: @@ -7892,12 +7866,6 @@ packages: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-dismissable-layer@1.0.3': - resolution: {integrity: sha512-nXZOvFjOuHS1ovumntGV7NNoLaEp9JEvTht3MBjP44NSW5hUKj/8OnfN3+8WmB+CEhN44XaGhpHoSsUIEl5P7Q==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-dismissable-layer@1.0.4': resolution: {integrity: sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==} peerDependencies: @@ -7911,12 +7879,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-dropdown-menu@2.0.4': - resolution: {integrity: sha512-y6AT9+MydyXcByivdK1+QpjWoKaC7MLjkS/cH1Q3keEyMvDkiY85m8o2Bi6+Z1PPUlCsMULopxagQOSfN0wahg==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-focus-guards@1.0.0': resolution: {integrity: sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==} peerDependencies: @@ -7937,12 +7899,6 @@ packages: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-focus-scope@1.0.2': - resolution: {integrity: sha512-spwXlNTfeIprt+kaEWE/qYuYT3ZAqJiAGjN/JgdvgVDTu8yc+HuX+WOWXrKliKnLnwck0F6JDkqIERncnih+4A==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-focus-scope@1.0.3': resolution: {integrity: sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==} peerDependencies: @@ -7970,18 +7926,6 @@ packages: '@types/react': optional: true - '@radix-ui/react-menu@2.0.4': - resolution: {integrity: sha512-mzKR47tZ1t193trEqlQoJvzY4u9vYfVH16ryBrVrCAGZzkgyWnMQYEZdUkM7y8ak9mrkKtJiqB47TlEnubeOFQ==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - - '@radix-ui/react-popper@1.1.1': - resolution: {integrity: sha512-keYDcdMPNMjSC8zTsZ8wezUMiWM9Yj14wtF3s0PTIs9srnEPC9Kt2Gny1T3T81mmSeyDjZxsD9N5WCwNNb712w==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-popper@1.1.2': resolution: {integrity: sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==} peerDependencies: @@ -8001,12 +7945,6 @@ packages: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-portal@1.0.2': - resolution: {integrity: sha512-swu32idoCW7KA2VEiUZGBSu9nB6qwGdV6k6HYhUoOo3M1FFpD+VgLzUqtt3mwL1ssz7r2x8MggpLSQach2Xy/Q==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-portal@1.0.3': resolution: {integrity: sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==} peerDependencies: @@ -8032,12 +7970,6 @@ packages: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-primitive@1.0.2': - resolution: {integrity: sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-primitive@1.0.3': resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} peerDependencies: @@ -8051,12 +7983,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-roving-focus@1.0.3': - resolution: {integrity: sha512-stjCkIoMe6h+1fWtXlA6cRfikdBzCLp3SnVk7c48cv/uy3DTGoXhN76YaOYUJuy3aEDvDIKwKR5KSmvrtPvQPQ==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-roving-focus@1.0.4': resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} peerDependencies: @@ -8101,11 +8027,6 @@ packages: peerDependencies: react: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-slot@1.0.1': - resolution: {integrity: sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-slot@1.0.2': resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: @@ -8187,11 +8108,6 @@ packages: peerDependencies: react: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-use-escape-keydown@1.0.2': - resolution: {integrity: sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-use-escape-keydown@1.0.3': resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==} peerDependencies: @@ -8224,11 +8140,6 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-rect@1.0.0': - resolution: {integrity: sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-use-rect@1.0.1': resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==} peerDependencies: @@ -8238,11 +8149,6 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-size@1.0.0': - resolution: {integrity: sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-use-size@1.0.1': resolution: {integrity: sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==} peerDependencies: @@ -8265,9 +8171,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/rect@1.0.0': - resolution: {integrity: sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg==} - '@radix-ui/rect@1.0.1': resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} @@ -10432,11 +10335,19 @@ packages: '@use-gesture/core@10.3.0': resolution: {integrity: sha512-rh+6MND31zfHcy9VU3dOZCqGY511lvGcfyJenN4cWZe0u1BH6brBpBddLVXhF2r4BMqWbvxfsbL7D287thJU2A==} + '@use-gesture/core@10.3.1': + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} + '@use-gesture/react@10.3.0': resolution: {integrity: sha512-3zc+Ve99z4usVP6l9knYVbVnZgfqhKah7sIG+PS2w+vpig2v2OLct05vs+ZXMzwxdNCMka8B+8WlOo0z6Pn6DA==} peerDependencies: react: '>= 16.8.0' + '@use-gesture/react@10.3.1': + resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==} + peerDependencies: + react: '>= 16.8.0' + '@webassemblyjs/ast@1.11.1': resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==} @@ -10693,6 +10604,10 @@ packages: resolution: {integrity: sha512-mOQtwpY5hUt4vMLyshZPPV1x9MBRF2FimUjIImfYJb1x8o6jY4npikzWplAfWYQUJJjWfw/1NmfqD7vUOh9+ww==} engines: {node: '>=12'} + '@wordpress/a11y@4.6.0': + resolution: {integrity: sha512-dSYGLgntqQCAiHBnNxttLOUZnH26m/BrIQdCXtb9JVJy5p68JAdFHbr6qFoOfOoTCvwUqE8cNS7K4GWfAJwT0w==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/api-fetch@3.23.1': resolution: {integrity: sha512-dmeigLuvqYAzpQ2hWUQT1P5VQAjkj9hS1z7PgNi1CcULFPbY8BWW+KiBETUu6Wm+rlSbUL2dC8qrA4JDv9ja5A==} @@ -10927,13 +10842,6 @@ packages: react: ^17.0.0 react-dom: ^17.0.0 - '@wordpress/components@25.13.0': - resolution: {integrity: sha512-Ym/5Xv7NnkJu40jCSmt/t6B8vT2ue2vobwDEz1FKlB0xGm5bzzh5589m2nZqqY459/Qm9dl5R4BKSdvKqKB2MQ==} - engines: {node: '>=12'} - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - '@wordpress/components@25.16.0': resolution: {integrity: sha512-voQuMsO5JbH+JW33TnWurwwvpSb8IQ4XU5wyVMubX4TUwadt+/2ToNJbZIDXoaJPei7vbM81Ft+pH+zGlN8CyA==} engines: {node: '>=12'} @@ -10955,6 +10863,13 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 + '@wordpress/components@28.6.0': + resolution: {integrity: sha512-9YmA+7Tmz19oOfKifOF/VxcwJwyyLK8Y2LupK7ge6Oue0P1bMLs/9LBgZUBizoKMWmXYdzBm8pXf9Eyqq3PG0Q==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + '@wordpress/compose@3.25.3': resolution: {integrity: sha512-tCO2EnJCkCH548OqA0uU8V1k/1skz2QwBlHs8ZQSpimqUS4OWWsAlndCEFe4U4vDTqFt2ow7tzAir+05Cw8MAg==} @@ -10992,6 +10907,12 @@ packages: peerDependencies: react: ^18.0.0 + '@wordpress/compose@7.6.0': + resolution: {integrity: sha512-4ukiLfCOUkb0zmdFpPSVOnQkpNHTWqQUOCgpMykjKO0gRfa/rZ6dxcZUQ/KEYT5EKZkGCo9bR4lBhxjNVrgfug==} + 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'} @@ -11042,6 +10963,12 @@ packages: peerDependencies: react: ^17.0.0 + '@wordpress/data@10.6.0': + resolution: {integrity: sha512-u6g1IeK3Vv0Ulr/0jPWU5wpde+flWH1SDvqgc50GjG2v03NWqzie8zTGGeHo8Fque7s/UNbGYKlzrbM3+dPl5g==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + '@wordpress/data@4.27.3': resolution: {integrity: sha512-5763NgNV9IIa1CC3Q80dAvrH6108tJtj3IrHfUCZmUk1atSNsOMBCkLdQ7tGTTi2JFejeGEMg1LJI22JD5zM6Q==} @@ -11081,14 +11008,16 @@ packages: peerDependencies: react: ^18.0.0 + '@wordpress/dataviews@4.2.0': + resolution: {integrity: sha512-rCnMbEVXKZYgQmJO7S448KPVh78DTHgfJ+B5H937l/HX8+Gd0OlkpbKi4C4UZUj0k/xwY7ccKERYurq3W8/NFg==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + '@wordpress/date@4.44.0': resolution: {integrity: sha512-WrSAg+gbRN5YB/YZhQnJMNKj80efc+6taVYq3VjSzp27CPxh75qTE5N56TJWGKZbB8mqCIEWy6eOXhIoBW19mQ==} engines: {node: '>=12'} - '@wordpress/date@4.47.0': - resolution: {integrity: sha512-HIruX+wMaQWKYLCFIu6JeEEoqRYkhpL4cWfZ1lJG78wNsgq3vRiHzXQaXHcbmJQCq0PZOxtmeSzldPiUMFVNpg==} - engines: {node: '>=12'} - '@wordpress/date@4.57.0': resolution: {integrity: sha512-azUXRQDhxoCkME7c+0Cw/aCZmyoQeTXhWJYtZBFyPU5wsIXSv/Ucp3WggJR7OSKFnE5rSp5qpCt/nihfLLfZWQ==} engines: {node: '>=12'} @@ -11097,6 +11026,10 @@ 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==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/dependency-extraction-webpack-plugin@2.9.0': resolution: {integrity: sha512-Eo8ByPd3iZ6az4UmdLD2xYLp1/7os/H80l28Y5OlS4DozkD3vcWCBReynWoBax74u3oJ9wWN5b/8oSxGwIKXYQ==} peerDependencies: @@ -11133,6 +11066,10 @@ packages: resolution: {integrity: sha512-ilOkjXejcnJMxnq1gTVkBnDPP9W+XjlEe1TIfaMKcCwKsfsNy6bgURxWl1qIM2dPjH+5KK65bPjW0XELTMJy4w==} engines: {node: '>=12'} + '@wordpress/deprecated@4.6.0': + resolution: {integrity: sha512-XQbF7SIb43I4Ey7nEDqowm7YJgzoUpdmZfNBN01/UXKUZ0FNaKzf2LCNjOCwfEfRE7AroyUgMR40qWVBBs+GKQ==} + 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'} @@ -11149,6 +11086,10 @@ packages: resolution: {integrity: sha512-G6OnfLLC0MIWi9efTW6DMNEtEoy7tCoV0MxD19gUuG3/rDOi8RgHYwuNCvt6ieQMISiLiHnsg4tZc4D45zdfZA==} engines: {node: '>=12'} + '@wordpress/dom-ready@4.6.0': + resolution: {integrity: sha512-3fX1O1abmp3++FpZMPnDQygeygUggqfEvWQQQ80di/ksMEo6DXvIdtXolwDQt9WIC1WetLdI7Mf3KKVJnruyxg==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/dom@2.18.0': resolution: {integrity: sha512-tM2WeQuSObl3nzWjUTF0/dyLnA7sdl/MXaSe32D64OF89bjSyJvjUipI7gjKzI3kJ7ddGhwcTggGvSB06MOoCQ==} @@ -11156,10 +11097,6 @@ packages: resolution: {integrity: sha512-ympP0cK4ErQSFCRyrhjg8wAK7Wb5NqTUyiw1kV+2TQ35PKNG+TCXjYkk19Wc0kxiYZPFtbxk8OPp40e8Up7y7g==} engines: {node: '>=12'} - '@wordpress/dom@3.47.0': - resolution: {integrity: sha512-SY6wfAc4yrXYil8fm/uyeKQnPjGuc0G9Q1/5pUKO6dssst8fClsrxy+hXNl0FYFGWnAZBqg5ccrwYydrFt5k/g==} - engines: {node: '>=12'} - '@wordpress/dom@3.57.0': resolution: {integrity: sha512-3vJ1Z5Lzb7kfMoB8ni275vFGIRrljWFQ2XsVfO6oA/HeoIfHAGVcR58GmbjyxwEgClrizMGIkbs9ubrRpontLQ==} engines: {node: '>=12'} @@ -11168,6 +11105,10 @@ packages: resolution: {integrity: sha512-wdWBzfxU8iUPpxxTACkFpYbEoC0f+Hqs24IYOkhn/8ERp2LFpUdFcwF7/DmY6agSpUs8iWT/2hSGdUz9Lw2f0w==} engines: {node: '>=12'} + '@wordpress/dom@4.6.0': + resolution: {integrity: sha512-ZCjMOya5dTkzgp/vTq7w1qpvVQDPoF7sJpalARUUQjeMUkUw/PTLYvvXJ3gARBCgaEdD85QjLorpxnJVz1XNng==} + 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'} @@ -11273,6 +11214,10 @@ packages: resolution: {integrity: sha512-/d/lWBDYYgzE2yeXYvPnjMSDG1EdQs5TSLdjM/drQVJMxWayFqAPaF/pVczLHCPYfjgyJN4Zc+bneAKj6dEiLw==} engines: {node: '>=12'} + '@wordpress/element@6.6.0': + resolution: {integrity: sha512-IvSocvmd0fNus/XZo7K1EU4UD7aOKUdi3Y7pFUW2ljBbL3vuXk3E+6bwYahCjUIlBhpgGuCjemWTdg2Awzfmiw==} + 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'} @@ -11289,6 +11234,10 @@ packages: resolution: {integrity: sha512-DkTDo1Qhvs9rfobBpg5vXAOKaev3Jox8R5ryvYIhql5chrkj/V5k2ZzwUChFXxYmivVkWacCwDGmDmwe2ex/ag==} engines: {node: '>=12'} + '@wordpress/escape-html@3.6.0': + resolution: {integrity: sha512-NY9As0uJ81TPTogBzD6G/m7L4+sjvkjTEKkNsHLD5aEYxRX+RHlPYPyyd6y4CmlOkttwymbV9eKNP+LrfX5zZQ==} + 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'} @@ -11358,8 +11307,8 @@ packages: resolution: {integrity: sha512-4sIngmH64M1jzcprfkffo1GHsQbd/QNbTweq6cSPIJNorKfE63Inf59NQ6r0pq6+Nz+cuq64eMz5v4eyngjZ/A==} engines: {node: '>=12'} - '@wordpress/hooks@4.4.0': - resolution: {integrity: sha512-KO0gUx0KLhH3XCatg9ZOU1TH0fgyQUccAEIM8liErfgmrabHl8JhDoR2Uk5k0jNKZNPog7XxvKgPFVtCzvzQig==} + '@wordpress/hooks@4.6.0': + resolution: {integrity: sha512-FWJhubBXeyRhx12YUmxT9pNoV9Azvx8nkynhduV+RNgA+F2SXoOf15pr+USPV//m3Bx031GN/wPHjgUCbC6+XA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} '@wordpress/html-entities@3.24.0': @@ -11378,6 +11327,10 @@ packages: resolution: {integrity: sha512-Nb0nCYIdTEehWJ6HoA76bxpseKDY/12rYZ10eqf5OSr6oMvtyJ5j4fkNMKuHFQ00Mhppl9fkYWp2c8ZzBcp5Vw==} engines: {node: '>=12'} + '@wordpress/html-entities@4.6.0': + resolution: {integrity: sha512-ypTlGwDKw7jpmu9rneErkkq9dFHXzju8SGdEWkVAeqhRS9Ifri9DvmrovASB2c5IPY+Ijwh4YlVkx1yNBRHr5w==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/i18n@3.20.0': resolution: {integrity: sha512-SIoOJFB4UrrYAScS4H91CYCLW9dX3Ghv8pBKc/yHGculb1AdGr6gRMlmJxZV62Cn3CZ4Ga86c+FfR+GiBu0JPg==} hasBin: true @@ -11392,11 +11345,6 @@ packages: engines: {node: '>=12'} hasBin: true - '@wordpress/i18n@4.54.0': - resolution: {integrity: sha512-gSKBopBN9rY9GhNy3CXLK3n4D5viuBTObvcu3blu4SFqkHl+Ws1Gx0tHbpypfV80ESrOyMXHJIAqWgBD8d4Hew==} - engines: {node: '>=12'} - hasBin: true - '@wordpress/i18n@4.57.0': resolution: {integrity: sha512-VYWYHE+7NxnZvE9Swhhe4leQcn0jHNkzRAEV36TkfAL/MvrQYCRh71KLTvKhsilG96HUQdBwjH0VPLmYEmR3sg==} engines: {node: '>=12'} @@ -11412,6 +11360,15 @@ packages: engines: {node: '>=18.12.0', npm: '>=8.19.2'} hasBin: true + '@wordpress/i18n@5.6.0': + resolution: {integrity: sha512-xTpwuRh0owYFlgRHUbUAQIWr8ye3FC0ZsjDIOskJaNkrheAU9ZWKJDcmQmPvi01Udml4g9LUIaffkcRd2kyW2g==} + 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@4.1.0': resolution: {integrity: sha512-1FpEjT9kJbr0cWbgdgIwd2DoeerWijcVx3qCZ/WMFKNElBH9lfZLuWPI1hpX102HGWFcEi3VlbVpdBGeCeYQWg==} engines: {node: '>=12'} @@ -11432,10 +11389,6 @@ packages: resolution: {integrity: sha512-QkJRDNgSJzfU3OCVr5X9P3Au3MIag2yT4dzM3Ej6VfrF0SPfFgMwroXKSdNEHmCCG7AwtzGOjaqjpQ3y9vRMkA==} engines: {node: '>=12'} - '@wordpress/icons@9.38.0': - resolution: {integrity: sha512-K+rSZM1eKuWh+rXeMWNLj4dT0a3RJSzoUUh9UDQZCSdnThyAyZECGEKfHSCfd28/yabxLKaziXrb5/MVBrPjZw==} - engines: {node: '>=12'} - '@wordpress/icons@9.48.0': resolution: {integrity: sha512-47efXMuqX8Qbf7sFyYeUJ0TPjs3tNqnjHUn3WGc7Gq1IIYD6EGYFmCzPAfciUIXwRBhez2oC4y6IAXl5GP3KBw==} engines: {node: '>=12'} @@ -11473,6 +11426,10 @@ packages: resolution: {integrity: sha512-qUMqZMLlunwY2J31HG6NZwD2kBIqcwvIDBmdQYvVuQ2aDGeB2Z6sVPXyHCqGfh2ynFfaIL8bDtjW5UtYGPUI4A==} engines: {node: '>=12'} + '@wordpress/is-shallow-equal@5.6.0': + resolution: {integrity: sha512-WjxXleJePz9scpTXMTl//mn3AgEBqdHd56pWtaDgz9Ub7O5H8AMNa2BU4VDK8OOQ3iwpAUgqGhaTRK5GjbaeSA==} + 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'} @@ -11572,6 +11529,10 @@ packages: resolution: {integrity: sha512-GLKho4gAFbqgmP3GxEPP5iSS2WwOtqX0xL0zVjElNC/uHKCULyZ2UlyDAc2clN5wiVNf3hC4A1BsxzKeKIMNFQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/keycodes@4.6.0': + resolution: {integrity: sha512-7jmKM1BLyoQPLXFl+3FPaKBrLEe7kUIkBMGS88083SQtXXFcW8sYQt5jd6E1yY6EAnniGveUNrv0C9Lbaipx3w==} + 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'} @@ -11723,6 +11684,12 @@ packages: resolution: {integrity: sha512-4vMhlu40+qxkt6lyCv2KWCx9bP7hcpPC9GXj9Kq3gwKIzSSHoqbYs3V8HYeGWrG9g7JWMFN9Pkdy8Bm61ZsKuQ==} engines: {node: '>=12'} + '@wordpress/primitives@4.6.0': + resolution: {integrity: sha512-uu4ANmgwslB2YOyIBQDSwKTQXXqGDL9Gz5INe+UeJZBMt2uU/TGEjKcZ63dqbuM8mqlPAcdVGL52RCt7mIKEhQ==} + 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==} @@ -11734,6 +11701,10 @@ packages: resolution: {integrity: sha512-g4Oka9aQFVPQUhXkKhHT6BoyTEdCG6S0TUvP4SP16PbkhbvIFwZ25GRQb2ERCVTdseCuDIM5YP0kwZd3NqTlGg==} engines: {node: '>=12'} + '@wordpress/priority-queue@3.6.0': + resolution: {integrity: sha512-r2cyisWaqDLesIqC8BqWoXyNIxt1lwjvevw5Kijl9zxzxfYBsNQlu7RI1JNYgnjbDQQirWukFgprt7tdzhwssQ==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/private-apis@0.20.0': resolution: {integrity: sha512-byyPRUNAD8/ca9N8gP2rUr8DHuMbzSoXO03nP8g3cecTN6iCOWqFDm6adkrbiAX527N9Ip+GOrRJd7Tta4kRIg==} engines: {node: '>=12'} @@ -11777,6 +11748,12 @@ packages: peerDependencies: redux: '>=4' + '@wordpress/redux-routine@5.6.0': + resolution: {integrity: sha512-CQkO+JZefPJLNBh5iBup2DRCXfUoPfEZeo2mhO91tSbBmrP08v1Pdk6YLsa8gNDXp4qJbFhNHMGCqRzEioMOhA==} + 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'} @@ -11807,15 +11784,15 @@ packages: peerDependencies: react: ^17.0.0 - '@wordpress/rich-text@6.24.0': - resolution: {integrity: sha512-RkvzK8zvLgpd7i5dlL6zs+Dig1lZNSZf/3sYyjX6RalISXNuxF6Zn8Or7kBcq7EcYmey0LMlVIl5FTZ2l7HSIA==} + '@wordpress/rich-text@6.34.0': + resolution: {integrity: sha512-qeHPgSaI6UolAA9s8ShlbqjWtlh1kTIOMKATDZD6GOACZurXh9ZVJxxsE95FSmLEu4SDmYJ0b2sZlh92yJuaPw==} engines: {node: '>=12'} peerDependencies: react: ^18.0.0 - '@wordpress/rich-text@6.34.0': - resolution: {integrity: sha512-qeHPgSaI6UolAA9s8ShlbqjWtlh1kTIOMKATDZD6GOACZurXh9ZVJxxsE95FSmLEu4SDmYJ0b2sZlh92yJuaPw==} - engines: {node: '>=12'} + '@wordpress/rich-text@7.6.0': + resolution: {integrity: sha512-XxlfrlwfCPX7f3u9DMinouYNM9PDBMeGZb4MlK2Fbrc8ympaTZOdH4U74VR3jgv0Eusx6vxFEA5JVVXpW/xS2w==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} peerDependencies: react: ^18.0.0 @@ -11926,6 +11903,10 @@ packages: resolution: {integrity: sha512-WhMKX/ETGUJr2GkaPgGwFF8gTU/PgikfvE2b2ZDjUglxIPYnujBa9S6w+kQPzwGniGJutHL1DFK+TmAaxoci9A==} engines: {node: '>=12'} + '@wordpress/undo-manager@1.6.0': + resolution: {integrity: sha512-Sl2rG/7t5zTQOgp+jOPn5m27sKd1DJIX/EGhM6LtRcjXZqa0rLDJXal1xWfkZk5oghaqW1TAwXJsg9UdAlh7Nw==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/url@2.22.2': resolution: {integrity: sha512-aqpYKQXzyzkCOm+GzZRYlLb+wh58g0cwR1PaKAl0UXaBS4mdS+X6biMriylb4P8CVC/RR7CSw5XI20JC24KDwQ==} @@ -11978,6 +11959,10 @@ packages: resolution: {integrity: sha512-Xs37x0IkvNewPNKs1A8cnw5xLb+AqwUqqCsH4+5Sjat5GDqP86mHgLfRIlE4d6fBYg+q6tO7DVPG49TT3/wzgA==} engines: {node: '>=12'} + '@wordpress/warning@3.6.0': + resolution: {integrity: sha512-pm57z1LZkzfQsXsji6yxcP0XSymKbvP087vJLlMkmLf+MoNVyTD6UvFpXl8hRSH6C6pySoJSgGFXaH81CRuO2Q==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/widgets@3.24.0': resolution: {integrity: sha512-bgjUoBjHKhyM2u7QrTScll7hCFDrHw0OxZWGbPXOGfE0VUgaej/d8QV5re7I+sOIi0g8+XLYQE0fwEyANt1iUg==} peerDependencies: @@ -16078,6 +16063,20 @@ packages: react-dom: optional: true + framer-motion@11.3.30: + resolution: {integrity: sha512-9VmqGe9OIjfMoCcs+ZsKXlv6JaG5QagKX2F1uSbkG3Z33wgjnz60Kw+CngC1M49rDYau+Y9aL+8jGagAwrbVyw==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + framer-motion@6.5.1: resolution: {integrity: sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==} peerDependencies: @@ -25466,10 +25465,10 @@ snapshots: '@ariakit/core@0.3.11': {} - '@ariakit/core@0.3.8': {} - '@ariakit/core@0.4.5': {} + '@ariakit/core@0.4.9': {} + '@ariakit/react-core@0.3.14(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: '@ariakit/core': 0.3.11 @@ -25486,9 +25485,9 @@ snapshots: react-dom: 18.3.1(react@18.3.1) use-sync-external-store: 1.2.0(react@18.3.1) - '@ariakit/react-core@0.3.9(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + '@ariakit/react-core@0.4.10(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: - '@ariakit/core': 0.3.8 + '@ariakit/core': 0.4.9 '@floating-ui/dom': 1.5.3 react: 17.0.2 react-dom: 17.0.2(react@17.0.2) @@ -25514,9 +25513,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@ariakit/react@0.3.9(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + '@ariakit/react@0.4.10(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: - '@ariakit/react-core': 0.3.9(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@ariakit/react-core': 0.4.10(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) @@ -25703,7 +25702,7 @@ snapshots: '@wordpress/primitives': 3.55.0 '@wordpress/react-i18n': 3.55.0 classnames: 2.3.2 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) 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) @@ -25807,7 +25806,7 @@ snapshots: '@babel/traverse': 7.23.5 '@babel/types': 7.23.5 convert-source-map: 1.9.0 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 lodash: 4.17.21 @@ -25830,7 +25829,7 @@ snapshots: '@babel/traverse': 7.23.5 '@babel/types': 7.23.5 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -25850,7 +25849,7 @@ snapshots: '@babel/traverse': 7.23.5 '@babel/types': 7.23.5 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -29949,7 +29948,7 @@ snapshots: '@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@17.0.71)(react@17.0.2))(@types/react@17.0.71)(react@17.0.2)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@emotion/babel-plugin': 11.11.0 '@emotion/is-prop-valid': 1.2.1 '@emotion/react': 11.11.1(@types/react@17.0.71)(react@17.0.2) @@ -29964,7 +29963,7 @@ snapshots: '@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@17.0.71)(react@18.3.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/is-prop-valid': 1.2.1 '@emotion/react': 11.11.1(@types/react@17.0.71)(react@18.3.1) @@ -30132,8 +30131,6 @@ snapshots: '@floating-ui/core@0.6.2': {} - '@floating-ui/core@0.7.3': {} - '@floating-ui/core@1.5.2': dependencies: '@floating-ui/utils': 0.1.6 @@ -30142,10 +30139,6 @@ snapshots: dependencies: '@floating-ui/core': 0.6.2 - '@floating-ui/dom@0.5.4': - dependencies: - '@floating-ui/core': 0.7.3 - '@floating-ui/dom@1.5.3': dependencies: '@floating-ui/core': 1.5.2 @@ -30169,27 +30162,12 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@floating-ui/react-dom@0.7.2(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': - dependencies: - '@floating-ui/dom': 0.5.4 - react: 17.0.2 - react-dom: 17.0.2(react@17.0.2) - use-isomorphic-layout-effect: 1.1.2(@types/react@17.0.71)(react@17.0.2) - transitivePeerDependencies: - - '@types/react' - '@floating-ui/react-dom@1.3.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: '@floating-ui/dom': 1.5.3 react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - '@floating-ui/react-dom@2.0.4(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': - dependencies: - '@floating-ui/dom': 1.5.3 - react: 17.0.2 - react-dom: 17.0.2(react@17.0.2) - '@floating-ui/react-dom@2.0.4(react-dom@18.3.1(react@17.0.2))(react@17.0.2)': dependencies: '@floating-ui/dom': 1.5.3 @@ -31560,7 +31538,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@9.4.0) + debug: 4.3.4(supports-color@8.1.1) fs-extra: 9.1.0 http-call: 5.3.0 load-json-file: 5.3.0 @@ -32030,7 +32008,7 @@ snapshots: '@puppeteer/browsers@1.4.6(typescript@5.3.2)': dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.3.0 @@ -32044,7 +32022,7 @@ snapshots: '@puppeteer/browsers@1.4.6(typescript@5.3.3)': dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.3.0 @@ -32058,7 +32036,7 @@ snapshots: '@puppeteer/browsers@1.9.0': dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.3.1 @@ -32080,13 +32058,6 @@ snapshots: dependencies: '@babel/runtime': 7.25.0 - '@radix-ui/react-arrow@1.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': - dependencies: - '@babel/runtime': 7.25.0 - '@radix-ui/react-primitive': 1.0.2(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) - '@radix-ui/react-arrow@1.0.3(@types/react-dom@18.0.10)(@types/react@17.0.71)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.0 @@ -32107,16 +32078,6 @@ snapshots: '@types/react': 17.0.71 '@types/react-dom': 18.3.0 - '@radix-ui/react-collection@1.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': - dependencies: - '@babel/runtime': 7.25.0 - '@radix-ui/react-compose-refs': 1.0.0(react@17.0.2) - '@radix-ui/react-context': 1.0.0(react@17.0.2) - '@radix-ui/react-primitive': 1.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@radix-ui/react-slot': 1.0.1(react@17.0.2) - react: 17.0.2 - react-dom: 17.0.2(react@17.0.2) - '@radix-ui/react-collection@1.0.3(@types/react-dom@18.0.10)(@types/react@17.0.71)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.0 @@ -32235,11 +32196,6 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@radix-ui/react-direction@1.0.0(react@17.0.2)': - dependencies: - '@babel/runtime': 7.25.0 - react: 17.0.2 - '@radix-ui/react-direction@1.0.1(@types/react@17.0.71)(react@17.0.2)': dependencies: '@babel/runtime': 7.25.0 @@ -32276,17 +32232,6 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@radix-ui/react-dismissable-layer@1.0.3(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': - dependencies: - '@babel/runtime': 7.25.0 - '@radix-ui/primitive': 1.0.0 - '@radix-ui/react-compose-refs': 1.0.0(react@17.0.2) - '@radix-ui/react-primitive': 1.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@radix-ui/react-use-callback-ref': 1.0.0(react@17.0.2) - '@radix-ui/react-use-escape-keydown': 1.0.2(react@17.0.2) - react: 17.0.2 - react-dom: 17.0.2(react@17.0.2) - '@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.0.10)(@types/react@17.0.71)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.0 @@ -32315,21 +32260,6 @@ snapshots: '@types/react': 17.0.71 '@types/react-dom': 18.3.0 - '@radix-ui/react-dropdown-menu@2.0.4(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': - dependencies: - '@babel/runtime': 7.25.0 - '@radix-ui/primitive': 1.0.0 - '@radix-ui/react-compose-refs': 1.0.0(react@17.0.2) - '@radix-ui/react-context': 1.0.0(react@17.0.2) - '@radix-ui/react-id': 1.0.0(react@17.0.2) - '@radix-ui/react-menu': 2.0.4(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@radix-ui/react-primitive': 1.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@radix-ui/react-use-controllable-state': 1.0.0(react@17.0.2) - react: 17.0.2 - react-dom: 17.0.2(react@17.0.2) - transitivePeerDependencies: - - '@types/react' - '@radix-ui/react-focus-guards@1.0.0(react@17.0.2)': dependencies: '@babel/runtime': 7.25.0 @@ -32372,15 +32302,6 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@radix-ui/react-focus-scope@1.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': - dependencies: - '@babel/runtime': 7.25.0 - '@radix-ui/react-compose-refs': 1.0.0(react@17.0.2) - '@radix-ui/react-primitive': 1.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@radix-ui/react-use-callback-ref': 1.0.0(react@17.0.2) - react: 17.0.2 - react-dom: 17.0.2(react@17.0.2) - '@radix-ui/react-focus-scope@1.0.3(@types/react-dom@18.0.10)(@types/react@17.0.71)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.0 @@ -32433,50 +32354,6 @@ snapshots: optionalDependencies: '@types/react': 17.0.71 - '@radix-ui/react-menu@2.0.4(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': - dependencies: - '@babel/runtime': 7.25.0 - '@radix-ui/primitive': 1.0.0 - '@radix-ui/react-collection': 1.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@radix-ui/react-compose-refs': 1.0.0(react@17.0.2) - '@radix-ui/react-context': 1.0.0(react@17.0.2) - '@radix-ui/react-direction': 1.0.0(react@17.0.2) - '@radix-ui/react-dismissable-layer': 1.0.3(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@radix-ui/react-focus-guards': 1.0.0(react@17.0.2) - '@radix-ui/react-focus-scope': 1.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@radix-ui/react-id': 1.0.0(react@17.0.2) - '@radix-ui/react-popper': 1.1.1(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@radix-ui/react-portal': 1.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@radix-ui/react-presence': 1.0.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@radix-ui/react-primitive': 1.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@radix-ui/react-roving-focus': 1.0.3(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@radix-ui/react-slot': 1.0.1(react@17.0.2) - '@radix-ui/react-use-callback-ref': 1.0.0(react@17.0.2) - aria-hidden: 1.2.3 - react: 17.0.2 - react-dom: 17.0.2(react@17.0.2) - react-remove-scroll: 2.5.5(@types/react@17.0.71)(react@17.0.2) - transitivePeerDependencies: - - '@types/react' - - '@radix-ui/react-popper@1.1.1(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': - dependencies: - '@babel/runtime': 7.25.0 - '@floating-ui/react-dom': 0.7.2(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@radix-ui/react-arrow': 1.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@radix-ui/react-compose-refs': 1.0.0(react@17.0.2) - '@radix-ui/react-context': 1.0.0(react@17.0.2) - '@radix-ui/react-primitive': 1.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@radix-ui/react-use-callback-ref': 1.0.0(react@17.0.2) - '@radix-ui/react-use-layout-effect': 1.0.0(react@17.0.2) - '@radix-ui/react-use-rect': 1.0.0(react@17.0.2) - '@radix-ui/react-use-size': 1.0.0(react@17.0.2) - '@radix-ui/rect': 1.0.0 - react: 17.0.2 - react-dom: 17.0.2(react@17.0.2) - transitivePeerDependencies: - - '@types/react' - '@radix-ui/react-popper@1.1.2(@types/react-dom@18.0.10)(@types/react@17.0.71)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.0 @@ -32529,13 +32406,6 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@radix-ui/react-portal@1.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': - dependencies: - '@babel/runtime': 7.25.0 - '@radix-ui/react-primitive': 1.0.2(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) - '@radix-ui/react-portal@1.0.3(@types/react-dom@18.0.10)(@types/react@17.0.71)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.0 @@ -32586,13 +32456,6 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@radix-ui/react-primitive@1.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': - dependencies: - '@babel/runtime': 7.25.0 - '@radix-ui/react-slot': 1.0.1(react@17.0.2) - react: 17.0.2 - react-dom: 17.0.2(react@17.0.2) - '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.0.10)(@types/react@17.0.71)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.0 @@ -32613,21 +32476,6 @@ snapshots: '@types/react': 17.0.71 '@types/react-dom': 18.3.0 - '@radix-ui/react-roving-focus@1.0.3(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': - dependencies: - '@babel/runtime': 7.25.0 - '@radix-ui/primitive': 1.0.0 - '@radix-ui/react-collection': 1.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@radix-ui/react-compose-refs': 1.0.0(react@17.0.2) - '@radix-ui/react-context': 1.0.0(react@17.0.2) - '@radix-ui/react-direction': 1.0.0(react@17.0.2) - '@radix-ui/react-id': 1.0.0(react@17.0.2) - '@radix-ui/react-primitive': 1.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@radix-ui/react-use-callback-ref': 1.0.0(react@17.0.2) - '@radix-ui/react-use-controllable-state': 1.0.0(react@17.0.2) - react: 17.0.2 - react-dom: 17.0.2(react@17.0.2) - '@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.0.10)(@types/react@17.0.71)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.0 @@ -32756,12 +32604,6 @@ snapshots: '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) react: 18.3.1 - '@radix-ui/react-slot@1.0.1(react@17.0.2)': - dependencies: - '@babel/runtime': 7.25.0 - '@radix-ui/react-compose-refs': 1.0.0(react@17.0.2) - react: 17.0.2 - '@radix-ui/react-slot@1.0.2(@types/react@17.0.71)(react@17.0.2)': dependencies: '@babel/runtime': 7.25.0 @@ -32930,12 +32772,6 @@ snapshots: '@radix-ui/react-use-callback-ref': 1.0.0(react@18.3.1) react: 18.3.1 - '@radix-ui/react-use-escape-keydown@1.0.2(react@17.0.2)': - dependencies: - '@babel/runtime': 7.25.0 - '@radix-ui/react-use-callback-ref': 1.0.0(react@17.0.2) - react: 17.0.2 - '@radix-ui/react-use-escape-keydown@1.0.3(@types/react@17.0.71)(react@17.0.2)': dependencies: '@babel/runtime': 7.25.0 @@ -32990,12 +32826,6 @@ snapshots: optionalDependencies: '@types/react': 17.0.71 - '@radix-ui/react-use-rect@1.0.0(react@17.0.2)': - dependencies: - '@babel/runtime': 7.25.0 - '@radix-ui/rect': 1.0.0 - react: 17.0.2 - '@radix-ui/react-use-rect@1.0.1(@types/react@17.0.71)(react@17.0.2)': dependencies: '@babel/runtime': 7.25.0 @@ -33012,12 +32842,6 @@ snapshots: optionalDependencies: '@types/react': 17.0.71 - '@radix-ui/react-use-size@1.0.0(react@17.0.2)': - dependencies: - '@babel/runtime': 7.25.0 - '@radix-ui/react-use-layout-effect': 1.0.0(react@17.0.2) - react: 17.0.2 - '@radix-ui/react-use-size@1.0.1(@types/react@17.0.71)(react@17.0.2)': dependencies: '@babel/runtime': 7.25.0 @@ -33054,10 +32878,6 @@ snapshots: '@types/react': 17.0.71 '@types/react-dom': 18.3.0 - '@radix-ui/rect@1.0.0': - dependencies: - '@babel/runtime': 7.25.0 - '@radix-ui/rect@1.0.1': dependencies: '@babel/runtime': 7.25.0 @@ -34593,7 +34413,7 @@ snapshots: style-loader: 1.3.0(webpack@4.47.0(webpack-cli@3.3.12(webpack@5.89.0))) terser-webpack-plugin: 4.2.3(webpack@4.47.0(webpack-cli@3.3.12(webpack@5.89.0))) ts-dedent: 2.2.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@4.47.0(webpack-cli@3.3.12(webpack@5.89.0))))(webpack@4.47.0(webpack-cli@3.3.12(webpack@5.89.0))) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.89.0(webpack-cli@3.3.12)))(webpack@4.47.0(webpack-cli@3.3.12(webpack@5.89.0))) util-deprecate: 1.0.2 webpack: 4.47.0(webpack-cli@3.3.12(webpack@5.89.0)) webpack-dev-middleware: 3.7.3(webpack@4.47.0(webpack-cli@3.3.12(webpack@5.89.0))) @@ -34654,7 +34474,7 @@ snapshots: style-loader: 1.3.0(webpack@4.47.0) terser-webpack-plugin: 4.2.3(webpack@4.47.0) ts-dedent: 2.2.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.89.0))(webpack@4.47.0) + url-loader: 4.1.1(file-loader@6.2.0(webpack@4.47.0))(webpack@4.47.0) util-deprecate: 1.0.2 webpack: 4.47.0 webpack-dev-middleware: 3.7.3(webpack@4.47.0) @@ -35828,7 +35648,7 @@ snapshots: telejson: 6.0.8 terser-webpack-plugin: 4.2.3(webpack@4.47.0(webpack-cli@3.3.12(webpack@5.89.0))) ts-dedent: 2.2.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@4.47.0(webpack-cli@3.3.12(webpack@5.89.0))))(webpack@4.47.0(webpack-cli@3.3.12(webpack@5.89.0))) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.89.0(webpack-cli@3.3.12)))(webpack@4.47.0(webpack-cli@3.3.12(webpack@5.89.0))) util-deprecate: 1.0.2 webpack: 4.47.0(webpack-cli@3.3.12(webpack@5.89.0)) webpack-dev-middleware: 3.7.3(webpack@4.47.0(webpack-cli@3.3.12(webpack@5.89.0))) @@ -35878,7 +35698,7 @@ snapshots: telejson: 6.0.8 terser-webpack-plugin: 4.2.3(webpack@4.47.0) ts-dedent: 2.2.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.89.0))(webpack@4.47.0) + url-loader: 4.1.1(file-loader@6.2.0(webpack@4.47.0))(webpack@4.47.0) util-deprecate: 1.0.2 webpack: 4.47.0 webpack-dev-middleware: 3.7.3(webpack@4.47.0) @@ -37549,7 +37369,6 @@ snapshots: '@types/react-dom@18.3.0': dependencies: '@types/react': 17.0.71 - optional: true '@types/react-outside-click-handler@1.3.3': dependencies: @@ -38009,7 +37828,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@9.4.0) + debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 grapheme-splitter: 1.0.4 ignore: 5.3.0 @@ -38028,7 +37847,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@9.4.0) + debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 graphemer: 1.4.0 ignore: 5.3.0 @@ -38047,7 +37866,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@9.4.0) + debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 graphemer: 1.4.0 ignore: 5.3.0 @@ -38127,7 +37946,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@9.4.0) + debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 optionalDependencies: typescript: 5.3.2 @@ -38139,7 +37958,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.2) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 optionalDependencies: typescript: 5.3.2 @@ -38151,7 +37970,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 optionalDependencies: typescript: 5.3.3 @@ -38349,6 +38168,8 @@ snapshots: '@use-gesture/core@10.3.0': {} + '@use-gesture/core@10.3.1': {} + '@use-gesture/react@10.3.0(react@17.0.2)': dependencies: '@use-gesture/core': 10.3.0 @@ -38359,6 +38180,16 @@ snapshots: '@use-gesture/core': 10.3.0 react: 18.3.1 + '@use-gesture/react@10.3.1(react@17.0.2)': + dependencies: + '@use-gesture/core': 10.3.1 + react: 17.0.2 + + '@use-gesture/react@10.3.1(react@18.3.1)': + dependencies: + '@use-gesture/core': 10.3.1 + react: 18.3.1 + '@webassemblyjs/ast@1.11.1': dependencies: '@webassemblyjs/helper-numbers': 1.11.1 @@ -38828,7 +38659,7 @@ snapshots: '@wordpress/a11y@3.47.0': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@wordpress/dom-ready': 3.47.0 '@wordpress/i18n': 4.47.0 @@ -38844,6 +38675,12 @@ snapshots: '@wordpress/dom-ready': 3.27.0 '@wordpress/i18n': 4.6.1 + '@wordpress/a11y@4.6.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/dom-ready': 4.6.0 + '@wordpress/i18n': 5.6.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: '@babel/runtime': 7.25.0 @@ -38881,7 +38718,7 @@ snapshots: '@wordpress/api-fetch@6.44.0': dependencies: '@babel/runtime': 7.25.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/i18n': 4.57.0 '@wordpress/url': 3.48.0 '@wordpress/api-fetch@7.0.1': @@ -38896,7 +38733,7 @@ snapshots: '@wordpress/autop@3.47.0': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@wordpress/babel-plugin-import-jsx-pragma@1.1.3(@babel/core@7.12.9)': dependencies: @@ -38993,7 +38830,7 @@ snapshots: '@babel/plugin-transform-runtime': 7.23.4(@babel/core@7.24.7) '@babel/preset-env': 7.23.5(@babel/core@7.24.7) '@babel/preset-typescript': 7.23.3(@babel/core@7.24.7) - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@wordpress/babel-plugin-import-jsx-pragma': 4.30.0(@babel/core@7.24.7) '@wordpress/browserslist-config': 5.30.0 '@wordpress/warning': 2.47.0 @@ -39015,7 +38852,7 @@ snapshots: '@wordpress/blob@3.47.0': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@wordpress/blob@3.6.1': dependencies: @@ -39075,34 +38912,34 @@ snapshots: '@emotion/react': 11.11.1(@types/react@17.0.71)(react@17.0.2) '@emotion/styled': 11.11.0(@emotion/react@11.11.1(@types/react@17.0.71)(react@17.0.2))(@types/react@17.0.71)(react@17.0.2) '@react-spring/web': 9.7.3(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@wordpress/a11y': 3.47.0 + '@wordpress/a11y': 3.57.0 '@wordpress/api-fetch': 6.44.0 '@wordpress/blob': 3.47.0 '@wordpress/blocks': 12.24.0(react@17.0.2) '@wordpress/commands': 0.18.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/components': 25.16.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/compose': 6.24.0(react@17.0.2) - '@wordpress/data': 9.17.0(react@17.0.2) - '@wordpress/date': 4.47.0 - '@wordpress/deprecated': 3.47.0 - '@wordpress/dom': 3.47.0 + '@wordpress/compose': 6.34.0(react@17.0.2) + '@wordpress/data': 9.27.0(react@17.0.2) + '@wordpress/date': 4.57.0 + '@wordpress/deprecated': 3.57.0 + '@wordpress/dom': 3.57.0 '@wordpress/element': 5.34.0 - '@wordpress/escape-html': 2.47.0 + '@wordpress/escape-html': 2.57.0 '@wordpress/hooks': 3.57.0 - '@wordpress/html-entities': 3.47.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/html-entities': 3.57.0 + '@wordpress/i18n': 4.57.0 '@wordpress/icons': 9.48.0 - '@wordpress/is-shallow-equal': 4.47.0 + '@wordpress/is-shallow-equal': 4.57.0 '@wordpress/keyboard-shortcuts': 4.24.0(react@17.0.2) '@wordpress/keycodes': 3.57.0 '@wordpress/notices': 4.15.0(react@17.0.2) '@wordpress/preferences': 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) '@wordpress/private-apis': 0.29.0 - '@wordpress/rich-text': 6.24.0(react@17.0.2) + '@wordpress/rich-text': 6.34.0(react@17.0.2) '@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 @@ -39136,34 +38973,34 @@ snapshots: '@emotion/react': 11.11.1(@types/react@17.0.71)(react@18.3.1) '@emotion/styled': 11.11.0(@emotion/react@11.11.1(@types/react@17.0.71)(react@18.3.1))(@types/react@17.0.71)(react@18.3.1) '@react-spring/web': 9.7.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@wordpress/a11y': 3.47.0 + '@wordpress/a11y': 3.57.0 '@wordpress/api-fetch': 6.44.0 '@wordpress/blob': 3.47.0 '@wordpress/blocks': 12.24.0(react@18.3.1) '@wordpress/commands': 0.18.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@18.3.1(react@18.3.1))(react@18.3.1) '@wordpress/components': 25.16.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@18.3.1(react@18.3.1))(react@18.3.1) - '@wordpress/compose': 6.24.0(react@18.3.1) - '@wordpress/data': 9.17.0(react@18.3.1) - '@wordpress/date': 4.47.0 - '@wordpress/deprecated': 3.47.0 - '@wordpress/dom': 3.47.0 + '@wordpress/compose': 6.34.0(react@18.3.1) + '@wordpress/data': 9.27.0(react@18.3.1) + '@wordpress/date': 4.57.0 + '@wordpress/deprecated': 3.57.0 + '@wordpress/dom': 3.57.0 '@wordpress/element': 5.34.0 - '@wordpress/escape-html': 2.47.0 + '@wordpress/escape-html': 2.57.0 '@wordpress/hooks': 3.57.0 - '@wordpress/html-entities': 3.47.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/html-entities': 3.57.0 + '@wordpress/i18n': 4.57.0 '@wordpress/icons': 9.48.0 - '@wordpress/is-shallow-equal': 4.47.0 + '@wordpress/is-shallow-equal': 4.57.0 '@wordpress/keyboard-shortcuts': 4.24.0(react@18.3.1) '@wordpress/keycodes': 3.57.0 '@wordpress/notices': 4.15.0(react@18.3.1) '@wordpress/preferences': 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@18.3.1(react@18.3.1))(react@18.3.1) '@wordpress/private-apis': 0.29.0 - '@wordpress/rich-text': 6.24.0(react@18.3.1) + '@wordpress/rich-text': 6.34.0(react@18.3.1) '@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 @@ -39203,7 +39040,7 @@ snapshots: '@wordpress/compose': 5.5.0(react@18.3.1) '@wordpress/data': 6.15.0(react@18.3.1) '@wordpress/deprecated': 3.41.0 - '@wordpress/dom': 3.27.0 + '@wordpress/dom': 3.6.1 '@wordpress/element': 4.20.0 '@wordpress/hooks': 3.6.1 '@wordpress/html-entities': 3.24.0 @@ -39239,7 +39076,7 @@ snapshots: '@wordpress/block-editor@8.6.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)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@react-spring/web': 9.7.3(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@wordpress/a11y': 3.47.0 '@wordpress/api-fetch': 6.21.0 @@ -39379,32 +39216,32 @@ snapshots: '@wordpress/block-library@8.24.1(@babel/helper-module-imports@7.24.7)(@babel/types@7.25.2)(@preact/signals-core@1.5.1)(@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 - '@wordpress/a11y': 3.47.0 + '@wordpress/a11y': 3.57.0 '@wordpress/api-fetch': 6.44.0 '@wordpress/autop': 3.47.0 '@wordpress/blob': 3.47.0 '@wordpress/block-editor': 12.15.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/blocks': 12.24.0(react@17.0.2) '@wordpress/components': 25.16.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/compose': 6.24.0(react@17.0.2) + '@wordpress/compose': 6.34.0(react@17.0.2) '@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.17.0(react@17.0.2) - '@wordpress/date': 4.47.0 - '@wordpress/deprecated': 3.47.0 - '@wordpress/dom': 3.47.0 + '@wordpress/data': 9.27.0(react@17.0.2) + '@wordpress/date': 4.57.0 + '@wordpress/deprecated': 3.57.0 + '@wordpress/dom': 3.57.0 '@wordpress/element': 5.34.0 - '@wordpress/escape-html': 2.47.0 + '@wordpress/escape-html': 2.57.0 '@wordpress/hooks': 3.57.0 - '@wordpress/html-entities': 3.47.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/html-entities': 3.57.0 + '@wordpress/i18n': 4.57.0 '@wordpress/icons': 9.48.0 '@wordpress/interactivity': 3.0.1(@preact/signals-core@1.5.1) '@wordpress/keycodes': 3.57.0 '@wordpress/notices': 4.15.0(react@17.0.2) - '@wordpress/primitives': 3.45.0 + '@wordpress/primitives': 3.55.0 '@wordpress/private-apis': 0.29.0 '@wordpress/reusable-blocks': 4.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/rich-text': 6.24.0(react@17.0.2) + '@wordpress/rich-text': 6.34.0(react@17.0.2) '@wordpress/server-side-render': 4.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/url': 3.48.0 '@wordpress/viewport': 5.24.0(react@17.0.2) @@ -39435,7 +39272,7 @@ snapshots: '@wordpress/block-serialization-default-parser@4.47.0': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@wordpress/blocks@11.1.5(react@18.3.1)': dependencies: @@ -39446,7 +39283,7 @@ snapshots: '@wordpress/compose': 5.5.0(react@18.3.1) '@wordpress/data': 6.15.0(react@18.3.1) '@wordpress/deprecated': 3.41.0 - '@wordpress/dom': 3.27.0 + '@wordpress/dom': 3.6.1 '@wordpress/element': 4.20.0 '@wordpress/hooks': 3.6.1 '@wordpress/html-entities': 3.24.0 @@ -39530,7 +39367,7 @@ snapshots: '@wordpress/compose': 6.24.0(react@17.0.2) '@wordpress/data': 9.17.0(react@17.0.2) '@wordpress/deprecated': 3.47.0 - '@wordpress/dom': 3.47.0 + '@wordpress/dom': 3.57.0 '@wordpress/element': 5.24.0 '@wordpress/hooks': 3.57.0 '@wordpress/html-entities': 3.47.0 @@ -39560,7 +39397,7 @@ snapshots: '@wordpress/compose': 6.24.0(react@18.3.1) '@wordpress/data': 9.17.0(react@18.3.1) '@wordpress/deprecated': 3.47.0 - '@wordpress/dom': 3.47.0 + '@wordpress/dom': 3.57.0 '@wordpress/element': 5.24.0 '@wordpress/hooks': 3.57.0 '@wordpress/html-entities': 3.47.0 @@ -39641,9 +39478,9 @@ snapshots: dependencies: '@babel/runtime': 7.25.0 '@wordpress/components': 25.16.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.17.0(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/keyboard-shortcuts': 4.24.0(react@17.0.2) '@wordpress/private-apis': 0.20.0 @@ -39672,7 +39509,7 @@ snapshots: '@wordpress/compose': 4.2.0(react@18.3.1) '@wordpress/date': 4.44.0 '@wordpress/deprecated': 3.41.0 - '@wordpress/dom': 3.27.0 + '@wordpress/dom': 3.6.1 '@wordpress/element': 3.2.0 '@wordpress/hooks': 3.6.1 '@wordpress/i18n': 4.47.0 @@ -39820,7 +39657,7 @@ snapshots: '@wordpress/compose': 5.4.1(react@17.0.2) '@wordpress/date': 4.44.0 '@wordpress/deprecated': 3.41.0 - '@wordpress/dom': 3.27.0 + '@wordpress/dom': 3.6.1 '@wordpress/element': 4.4.1 '@wordpress/escape-html': 2.47.0 '@wordpress/hooks': 3.6.1 @@ -39868,7 +39705,7 @@ snapshots: '@wordpress/compose': 5.4.1(react@17.0.2) '@wordpress/date': 4.44.0 '@wordpress/deprecated': 3.41.0 - '@wordpress/dom': 3.27.0 + '@wordpress/dom': 3.6.1 '@wordpress/element': 4.4.1 '@wordpress/escape-html': 2.47.0 '@wordpress/hooks': 3.6.1 @@ -39904,7 +39741,7 @@ snapshots: '@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.24.7 + '@babel/runtime': 7.25.0 '@emotion/cache': 11.11.0 '@emotion/css': 11.11.2 '@emotion/react': 11.11.1(@types/react@17.0.71)(react@17.0.2) @@ -39925,7 +39762,7 @@ snapshots: '@wordpress/icons': 9.36.0 '@wordpress/is-shallow-equal': 4.24.0 '@wordpress/keycodes': 3.47.0 - '@wordpress/primitives': 3.45.0 + '@wordpress/primitives': 3.55.0 '@wordpress/rich-text': 5.20.0(react@17.0.2) '@wordpress/warning': 2.47.0 change-case: 4.1.2 @@ -39974,7 +39811,7 @@ snapshots: '@wordpress/icons': 9.36.0 '@wordpress/is-shallow-equal': 4.24.0 '@wordpress/keycodes': 3.47.0 - '@wordpress/primitives': 3.45.0 + '@wordpress/primitives': 3.55.0 '@wordpress/rich-text': 5.20.0(react@17.0.2) '@wordpress/warning': 2.47.0 change-case: 4.1.2 @@ -40001,70 +39838,6 @@ snapshots: - '@types/react' - supports-color - '@wordpress/components@25.13.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: - '@ariakit/react': 0.3.9(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@babel/runtime': 7.25.0 - '@emotion/cache': 11.11.0 - '@emotion/css': 11.11.2 - '@emotion/react': 11.11.1(@types/react@17.0.71)(react@17.0.2) - '@emotion/serialize': 1.1.2 - '@emotion/styled': 11.11.0(@emotion/react@11.11.1(@types/react@17.0.71)(react@17.0.2))(@types/react@17.0.71)(react@17.0.2) - '@emotion/utils': 1.2.1 - '@floating-ui/react-dom': 2.0.4(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@radix-ui/react-dropdown-menu': 2.0.4(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@types/gradient-parser': 0.1.3 - '@types/highlight-words-core': 1.2.1 - '@use-gesture/react': 10.3.0(react@17.0.2) - '@wordpress/a11y': 3.47.0 - '@wordpress/compose': 6.24.0(react@17.0.2) - '@wordpress/date': 4.47.0 - '@wordpress/deprecated': 3.47.0 - '@wordpress/dom': 3.47.0 - '@wordpress/element': 5.34.0 - '@wordpress/escape-html': 2.47.0 - '@wordpress/hooks': 3.57.0 - '@wordpress/html-entities': 3.47.0 - '@wordpress/i18n': 4.54.0 - '@wordpress/icons': 9.48.0 - '@wordpress/is-shallow-equal': 4.47.0 - '@wordpress/keycodes': 3.57.0 - '@wordpress/primitives': 3.45.0 - '@wordpress/private-apis': 0.29.0 - '@wordpress/rich-text': 6.24.0(react@17.0.2) - '@wordpress/warning': 2.47.0 - change-case: 4.1.2 - classnames: 2.3.2 - colord: 2.9.3 - date-fns: 2.30.0 - deepmerge: 4.3.1 - dom-scroll-into-view: 1.2.1 - downshift: 6.1.12(react@17.0.2) - fast-deep-equal: 3.1.3 - framer-motion: 10.16.16(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - gradient-parser: 0.1.5 - highlight-words-core: 1.2.2 - is-plain-object: 5.0.0 - memize: 2.1.0 - path-to-regexp: 6.2.1 - 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-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.5.0 - use-lilius: 2.0.3(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - uuid: 9.0.1 - valtio: 1.7.0(@babel/helper-module-imports@7.24.7)(@babel/types@7.25.2)(babel-plugin-macros@3.1.0)(react@17.0.2) - transitivePeerDependencies: - - '@babel/helper-module-imports' - - '@babel/types' - - '@types/react' - - aslemammad-vite-plugin-macro - - babel-plugin-macros - - supports-color - - vite - '@wordpress/components@25.16.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: '@ariakit/react': 0.3.14(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -40078,7 +39851,7 @@ snapshots: '@floating-ui/react-dom': 2.0.9(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@types/gradient-parser': 0.1.3 '@types/highlight-words-core': 1.2.1 - '@use-gesture/react': 10.3.0(react@17.0.2) + '@use-gesture/react': 10.3.1(react@17.0.2) '@wordpress/a11y': 3.57.0 '@wordpress/compose': 6.34.0(react@17.0.2) '@wordpress/date': 4.57.0 @@ -40141,7 +39914,7 @@ snapshots: '@floating-ui/react-dom': 2.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/gradient-parser': 0.1.3 '@types/highlight-words-core': 1.2.1 - '@use-gesture/react': 10.3.0(react@18.3.1) + '@use-gesture/react': 10.3.1(react@18.3.1) '@wordpress/a11y': 3.57.0 '@wordpress/compose': 6.34.0(react@18.3.1) '@wordpress/date': 4.57.0 @@ -40266,7 +40039,7 @@ snapshots: '@floating-ui/react-dom': 2.0.9(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@types/gradient-parser': 0.1.3 '@types/highlight-words-core': 1.2.1 - '@use-gesture/react': 10.3.0(react@17.0.2) + '@use-gesture/react': 10.3.1(react@17.0.2) '@wordpress/a11y': 3.57.0 '@wordpress/compose': 6.34.0(react@17.0.2) '@wordpress/date': 4.57.0 @@ -40308,6 +40081,61 @@ 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)': + dependencies: + '@ariakit/react': 0.4.10(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@babel/runtime': 7.25.0 + '@emotion/cache': 11.11.0 + '@emotion/css': 11.11.2 + '@emotion/react': 11.11.1(@types/react@17.0.71)(react@17.0.2) + '@emotion/serialize': 1.1.2 + '@emotion/styled': 11.11.0(@emotion/react@11.11.1(@types/react@17.0.71)(react@17.0.2))(@types/react@17.0.71)(react@17.0.2) + '@emotion/utils': 1.2.1 + '@floating-ui/react-dom': 2.0.9(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@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 + change-case: 4.1.2 + clsx: 2.1.1 + colord: 2.9.3 + date-fns: 3.6.0 + deepmerge: 4.3.1 + fast-deep-equal: 3.1.3 + framer-motion: 11.3.30(@emotion/is-prop-valid@1.2.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 + is-plain-object: 5.0.0 + memize: 2.1.0 + path-to-regexp: 6.2.1 + 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-dom: 17.0.2(react@17.0.2) + remove-accents: 0.5.0 + use-lilius: 2.0.5(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + uuid: 9.0.1 + transitivePeerDependencies: + - '@emotion/is-prop-valid' + - '@types/react' + - supports-color + '@wordpress/compose@3.25.3(react@17.0.2)': dependencies: '@babel/runtime': 7.25.0 @@ -40331,8 +40159,8 @@ snapshots: '@babel/runtime': 7.25.0 '@types/lodash': 4.14.149 '@types/mousetrap': 1.6.15 - '@wordpress/deprecated': 3.41.0 - '@wordpress/dom': 3.27.0 + '@wordpress/deprecated': 3.6.1 + '@wordpress/dom': 3.6.1 '@wordpress/element': 3.2.0 '@wordpress/is-shallow-equal': 4.24.0 '@wordpress/keycodes': 3.47.0 @@ -40350,7 +40178,7 @@ snapshots: '@babel/runtime': 7.25.0 '@types/mousetrap': 1.6.15 '@wordpress/deprecated': 3.47.0 - '@wordpress/dom': 3.47.0 + '@wordpress/dom': 3.27.0 '@wordpress/element': 4.20.0 '@wordpress/is-shallow-equal': 4.47.0 '@wordpress/keycodes': 3.47.0 @@ -40366,7 +40194,7 @@ snapshots: '@babel/runtime': 7.25.0 '@types/mousetrap': 1.6.15 '@wordpress/deprecated': 3.47.0 - '@wordpress/dom': 3.47.0 + '@wordpress/dom': 3.27.0 '@wordpress/element': 4.20.0 '@wordpress/is-shallow-equal': 4.47.0 '@wordpress/keycodes': 3.47.0 @@ -40383,7 +40211,7 @@ snapshots: '@types/lodash': 4.14.202 '@types/mousetrap': 1.6.15 '@wordpress/deprecated': 3.6.1 - '@wordpress/dom': 3.27.0 + '@wordpress/dom': 3.6.1 '@wordpress/element': 4.4.1 '@wordpress/is-shallow-equal': 4.24.0 '@wordpress/keycodes': 3.6.1 @@ -40397,7 +40225,7 @@ snapshots: '@wordpress/compose@5.5.0(react@17.0.2)': dependencies: - '@babel/runtime': 7.23.6 + '@babel/runtime': 7.25.0 '@types/lodash': 4.14.202 '@types/mousetrap': 1.6.15 '@wordpress/deprecated': 3.41.0 @@ -40415,7 +40243,7 @@ snapshots: '@wordpress/compose@5.5.0(react@18.3.1)': dependencies: - '@babel/runtime': 7.23.6 + '@babel/runtime': 7.25.0 '@types/lodash': 4.14.202 '@types/mousetrap': 1.6.15 '@wordpress/deprecated': 3.41.0 @@ -40435,8 +40263,8 @@ snapshots: dependencies: '@babel/runtime': 7.25.0 '@types/mousetrap': 1.6.15 - '@wordpress/deprecated': 3.47.0 - '@wordpress/dom': 3.47.0 + '@wordpress/deprecated': 3.57.0 + '@wordpress/dom': 3.57.0 '@wordpress/element': 5.34.0 '@wordpress/is-shallow-equal': 4.47.0 '@wordpress/keycodes': 3.47.0 @@ -40452,8 +40280,8 @@ snapshots: dependencies: '@babel/runtime': 7.25.0 '@types/mousetrap': 1.6.15 - '@wordpress/deprecated': 3.47.0 - '@wordpress/dom': 3.47.0 + '@wordpress/deprecated': 3.57.0 + '@wordpress/dom': 3.57.0 '@wordpress/element': 5.34.0 '@wordpress/is-shallow-equal': 4.47.0 '@wordpress/keycodes': 3.47.0 @@ -40499,12 +40327,29 @@ snapshots: react: 18.3.1 use-memo-one: 1.1.3(react@18.3.1) + '@wordpress/compose@7.6.0(react@17.0.2)': + dependencies: + '@babel/runtime': 7.25.0 + '@types/mousetrap': 1.6.15 + '@wordpress/deprecated': 4.6.0 + '@wordpress/dom': 4.6.0 + '@wordpress/element': 6.6.0 + '@wordpress/is-shallow-equal': 5.6.0 + '@wordpress/keycodes': 4.6.0 + '@wordpress/priority-queue': 3.6.0 + '@wordpress/undo-manager': 1.6.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 '@wordpress/commands': 0.9.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/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.17.0(react@17.0.2) + '@wordpress/data': 9.27.0(react@17.0.2) '@wordpress/element': 5.22.0 '@wordpress/i18n': 4.47.0 '@wordpress/icons': 9.36.0 @@ -40707,6 +40552,25 @@ snapshots: '@wordpress/deprecated': 3.41.0 react: 17.0.2 + '@wordpress/data@10.6.0(react@17.0.2)': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/compose': 7.6.0(react@17.0.2) + '@wordpress/deprecated': 4.6.0 + '@wordpress/element': 6.6.0 + '@wordpress/is-shallow-equal': 5.6.0 + '@wordpress/priority-queue': 3.6.0 + '@wordpress/private-apis': 1.6.0 + '@wordpress/redux-routine': 5.6.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 @@ -40733,8 +40597,8 @@ snapshots: '@wordpress/deprecated': 3.6.1 '@wordpress/element': 3.2.0 '@wordpress/is-shallow-equal': 4.24.0 - '@wordpress/priority-queue': 2.47.0 - '@wordpress/redux-routine': 4.47.0(redux@4.2.1) + '@wordpress/priority-queue': 2.57.0 + '@wordpress/redux-routine': 4.57.0(redux@4.2.1) equivalent-key-map: 0.2.2 is-promise: 4.0.0 lodash: 4.17.21 @@ -40747,7 +40611,7 @@ snapshots: '@wordpress/data@6.15.0(react@17.0.2)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@wordpress/compose': 5.20.0(react@17.0.2) '@wordpress/deprecated': 3.41.0 '@wordpress/element': 4.20.0 @@ -40817,7 +40681,7 @@ snapshots: '@wordpress/data@7.6.0(react@17.0.2)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@wordpress/compose': 5.20.0(react@17.0.2) '@wordpress/deprecated': 3.41.0 '@wordpress/element': 4.20.0 @@ -40835,7 +40699,7 @@ snapshots: '@wordpress/data@7.6.0(react@18.3.1)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@wordpress/compose': 5.20.0(react@18.3.1) '@wordpress/deprecated': 3.41.0 '@wordpress/element': 4.20.0 @@ -40927,6 +40791,28 @@ 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)': + 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 + clsx: 2.1.1 + react: 17.0.2 + remove-accents: 0.5.0 + transitivePeerDependencies: + - '@emotion/is-prop-valid' + - '@types/react' + - react-dom + - supports-color + '@wordpress/date@4.44.0': dependencies: '@babel/runtime': 7.23.5 @@ -40934,13 +40820,6 @@ snapshots: moment: 2.29.4 moment-timezone: 0.5.43 - '@wordpress/date@4.47.0': - dependencies: - '@babel/runtime': 7.25.0 - '@wordpress/deprecated': 3.47.0 - moment: 2.29.4 - moment-timezone: 0.5.43 - '@wordpress/date@4.57.0': dependencies: '@babel/runtime': 7.25.0 @@ -40954,6 +40833,13 @@ snapshots: moment: 2.29.4 moment-timezone: 0.5.43 + '@wordpress/date@5.6.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/deprecated': 4.6.0 + moment: 2.29.4 + moment-timezone: 0.5.43 + '@wordpress/dependency-extraction-webpack-plugin@2.9.0(webpack@4.47.0(webpack-cli@3.3.12))': dependencies: json2php: 0.0.4 @@ -41015,6 +40901,11 @@ snapshots: '@babel/runtime': 7.23.5 '@wordpress/hooks': 3.6.1 + '@wordpress/deprecated@4.6.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/hooks': 4.6.0 + '@wordpress/dom-ready@3.27.0': dependencies: '@babel/runtime': 7.23.5 @@ -41031,6 +40922,10 @@ snapshots: dependencies: '@babel/runtime': 7.23.5 + '@wordpress/dom-ready@4.6.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/dom@2.18.0': dependencies: '@babel/runtime': 7.25.0 @@ -41041,11 +40936,6 @@ snapshots: '@babel/runtime': 7.23.5 '@wordpress/deprecated': 3.41.0 - '@wordpress/dom@3.47.0': - dependencies: - '@babel/runtime': 7.25.0 - '@wordpress/deprecated': 3.47.0 - '@wordpress/dom@3.57.0': dependencies: '@babel/runtime': 7.25.0 @@ -41056,6 +40946,11 @@ snapshots: '@babel/runtime': 7.23.5 lodash: 4.17.21 + '@wordpress/dom@4.6.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/deprecated': 4.6.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 @@ -41134,7 +41029,7 @@ snapshots: '@wordpress/e2e-test-utils@4.16.1(encoding@0.1.13)(jest@25.5.4)(puppeteer@2.1.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@18.3.1))': dependencies: - '@babel/runtime': 7.25.0 + '@babel/runtime': 7.23.5 '@wordpress/keycodes': 2.19.3 '@wordpress/url': 2.22.2(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@18.3.1)) jest: 25.5.4 @@ -41160,7 +41055,7 @@ snapshots: '@wordpress/e2e-test-utils@4.16.1(encoding@0.1.13)(jest@29.7.0(@types/node@22.4.0)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.3.3)))(puppeteer@17.1.3(encoding@0.1.13))(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 + '@babel/runtime': 7.23.5 '@wordpress/keycodes': 2.19.3 '@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)) jest: 29.7.0(@types/node@22.4.0)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@22.4.0)(typescript@5.3.3)) @@ -41264,7 +41159,7 @@ snapshots: '@wordpress/element': 4.4.1 '@wordpress/hooks': 3.6.1 '@wordpress/i18n': 4.6.1 - '@wordpress/icons': 8.2.3 + '@wordpress/icons': 8.4.0 '@wordpress/interface': 4.5.6(@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) '@wordpress/keyboard-shortcuts': 3.4.1(react@17.0.2) '@wordpress/keycodes': 3.6.1 @@ -41288,36 +41183,36 @@ snapshots: '@wordpress/edit-site@5.15.0(patch_hash=6y3l6gxu33zybfmvbjd23dtqda)(@babel/helper-module-imports@7.24.7)(@babel/types@7.25.2)(@preact/signals-core@1.5.1)(@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.23.5 - '@wordpress/a11y': 3.47.0 + '@babel/runtime': 7.25.0 + '@wordpress/a11y': 3.57.0 '@wordpress/api-fetch': 6.44.0 '@wordpress/block-editor': 12.15.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/block-library': 8.24.1(@babel/helper-module-imports@7.24.7)(@babel/types@7.25.2)(@preact/signals-core@1.5.1)(@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/blocks': 12.24.0(react@17.0.2) '@wordpress/commands': 0.9.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/components': 25.13.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/compose': 6.24.0(react@17.0.2) + '@wordpress/components': 25.16.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/compose': 6.34.0(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) '@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.17.0(react@17.0.2) + '@wordpress/data': 9.27.0(react@17.0.2) '@wordpress/date': 4.44.0 '@wordpress/deprecated': 3.41.0 - '@wordpress/dom': 3.47.0 + '@wordpress/dom': 3.57.0 '@wordpress/editor': 13.24.1(@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/element': 5.22.0 - '@wordpress/escape-html': 2.47.0 + '@wordpress/escape-html': 2.57.0 '@wordpress/hooks': 3.57.0 - '@wordpress/html-entities': 3.47.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/html-entities': 3.57.0 + '@wordpress/i18n': 4.57.0 '@wordpress/icons': 9.36.0 '@wordpress/interface': 5.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/keyboard-shortcuts': 4.24.0(react@17.0.2) - '@wordpress/keycodes': 3.47.0 + '@wordpress/keycodes': 3.57.0 '@wordpress/media-utils': 4.38.0 '@wordpress/notices': 4.15.0(react@17.0.2) '@wordpress/plugins': 6.15.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/preferences': 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) - '@wordpress/primitives': 3.45.0 + '@wordpress/primitives': 3.55.0 '@wordpress/private-apis': 0.20.0 '@wordpress/reusable-blocks': 4.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/router': 0.7.0(react@17.0.2) @@ -41369,7 +41264,7 @@ snapshots: '@wordpress/hooks': 3.6.1 '@wordpress/html-entities': 3.6.1 '@wordpress/i18n': 4.6.1 - '@wordpress/icons': 8.2.3 + '@wordpress/icons': 8.4.0 '@wordpress/keyboard-shortcuts': 3.4.1(react@17.0.2) '@wordpress/keycodes': 3.6.1 '@wordpress/media-utils': 3.4.1 @@ -41395,22 +41290,22 @@ snapshots: '@wordpress/editor@13.24.1(@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 - '@wordpress/a11y': 3.47.0 + '@wordpress/a11y': 3.57.0 '@wordpress/api-fetch': 6.44.0 '@wordpress/blob': 3.47.0 '@wordpress/block-editor': 12.15.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/blocks': 12.24.0(react@17.0.2) '@wordpress/components': 25.16.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/compose': 6.24.0(react@17.0.2) + '@wordpress/compose': 6.34.0(react@17.0.2) '@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.17.0(react@17.0.2) - '@wordpress/date': 4.47.0 - '@wordpress/deprecated': 3.47.0 - '@wordpress/dom': 3.47.0 + '@wordpress/data': 9.27.0(react@17.0.2) + '@wordpress/date': 4.57.0 + '@wordpress/deprecated': 3.57.0 + '@wordpress/dom': 3.57.0 '@wordpress/element': 5.34.0 '@wordpress/hooks': 3.57.0 - '@wordpress/html-entities': 3.47.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/html-entities': 3.57.0 + '@wordpress/i18n': 4.57.0 '@wordpress/icons': 9.48.0 '@wordpress/keyboard-shortcuts': 4.24.0(react@17.0.2) '@wordpress/keycodes': 3.57.0 @@ -41420,7 +41315,7 @@ snapshots: '@wordpress/preferences': 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) '@wordpress/private-apis': 0.29.0 '@wordpress/reusable-blocks': 4.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/rich-text': 6.24.0(react@17.0.2) + '@wordpress/rich-text': 6.34.0(react@17.0.2) '@wordpress/server-side-render': 4.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/url': 3.48.0 '@wordpress/wordcount': 3.47.0 @@ -41517,6 +41412,17 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@wordpress/element@6.6.0': + dependencies: + '@babel/runtime': 7.25.0 + '@types/react': 17.0.71 + '@types/react-dom': 18.3.0 + '@wordpress/escape-html': 3.6.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 @@ -41546,6 +41452,10 @@ snapshots: dependencies: '@babel/runtime': 7.25.0 + '@wordpress/escape-html@3.6.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/eslint-plugin@12.9.0(@babel/core@7.24.7)(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.24.7 @@ -41715,13 +41625,13 @@ snapshots: '@wordpress/hooks@3.57.0': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@wordpress/hooks@3.6.1': dependencies: '@babel/runtime': 7.24.7 - '@wordpress/hooks@4.4.0': + '@wordpress/hooks@4.6.0': dependencies: '@babel/runtime': 7.25.0 @@ -41741,6 +41651,10 @@ snapshots: dependencies: '@babel/runtime': 7.23.5 + '@wordpress/html-entities@4.6.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/i18n@3.20.0': dependencies: '@babel/runtime': 7.25.0 @@ -41769,15 +41683,6 @@ snapshots: sprintf-js: 1.1.3 tannin: 1.2.0 - '@wordpress/i18n@4.54.0': - dependencies: - '@babel/runtime': 7.25.0 - '@wordpress/hooks': 3.57.0 - gettext-parser: 1.4.0 - memize: 2.1.0 - sprintf-js: 1.1.3 - tannin: 1.2.0 - '@wordpress/i18n@4.57.0': dependencies: '@babel/runtime': 7.25.0 @@ -41800,12 +41705,29 @@ snapshots: '@wordpress/i18n@5.0.1': dependencies: '@babel/runtime': 7.25.0 - '@wordpress/hooks': 4.4.0 + '@wordpress/hooks': 4.6.0 gettext-parser: 1.4.0 memize: 2.1.0 sprintf-js: 1.1.3 tannin: 1.2.0 + '@wordpress/i18n@5.6.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/hooks': 4.6.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 + '@wordpress/element': 6.6.0 + '@wordpress/primitives': 4.6.0(react@17.0.2) + transitivePeerDependencies: + - react + '@wordpress/icons@4.1.0': dependencies: '@babel/runtime': 7.25.0 @@ -41836,12 +41758,6 @@ snapshots: '@wordpress/element': 5.22.0 '@wordpress/primitives': 3.45.0 - '@wordpress/icons@9.38.0': - dependencies: - '@babel/runtime': 7.25.0 - '@wordpress/element': 5.34.0 - '@wordpress/primitives': 3.45.0 - '@wordpress/icons@9.48.0': dependencies: '@babel/runtime': 7.25.0 @@ -41867,7 +41783,7 @@ snapshots: '@wordpress/deprecated': 3.6.1 '@wordpress/element': 4.4.1 '@wordpress/i18n': 4.6.1 - '@wordpress/icons': 8.2.3 + '@wordpress/icons': 8.4.0 '@wordpress/plugins': 4.4.3(react@17.0.2) '@wordpress/preferences': 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) '@wordpress/viewport': 4.20.0(react@17.0.2) @@ -41882,15 +41798,15 @@ snapshots: '@wordpress/interface@5.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: - '@babel/runtime': 7.23.5 - '@wordpress/a11y': 3.47.0 - '@wordpress/components': 25.13.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/compose': 6.24.0(react@17.0.2) - '@wordpress/data': 9.17.0(react@17.0.2) - '@wordpress/deprecated': 3.47.0 - '@wordpress/element': 5.24.0 - '@wordpress/i18n': 4.47.0 - '@wordpress/icons': 9.38.0 + '@babel/runtime': 7.25.0 + '@wordpress/a11y': 3.57.0 + '@wordpress/components': 25.16.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/compose': 6.34.0(react@17.0.2) + '@wordpress/data': 9.27.0(react@17.0.2) + '@wordpress/deprecated': 3.57.0 + '@wordpress/element': 5.34.0 + '@wordpress/i18n': 4.57.0 + '@wordpress/icons': 9.48.0 '@wordpress/plugins': 6.15.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/preferences': 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) '@wordpress/viewport': 5.24.0(react@17.0.2) @@ -41922,6 +41838,10 @@ snapshots: dependencies: '@babel/runtime': 7.25.0 + '@wordpress/is-shallow-equal@5.6.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/jest-console@3.10.0(jest@25.5.4)': dependencies: '@babel/runtime': 7.25.0 @@ -42078,7 +41998,7 @@ snapshots: '@wordpress/keyboard-shortcuts@3.20.0(react@17.0.2)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@wordpress/data': 7.6.0(react@17.0.2) '@wordpress/element': 4.20.0 '@wordpress/keycodes': 3.47.0 @@ -42108,7 +42028,7 @@ snapshots: '@wordpress/keyboard-shortcuts@4.24.0(react@17.0.2)': dependencies: '@babel/runtime': 7.25.0 - '@wordpress/data': 9.17.0(react@17.0.2) + '@wordpress/data': 9.27.0(react@17.0.2) '@wordpress/element': 5.34.0 '@wordpress/keycodes': 3.57.0 react: 17.0.2 @@ -42117,7 +42037,7 @@ snapshots: '@wordpress/keyboard-shortcuts@4.24.0(react@18.3.1)': dependencies: '@babel/runtime': 7.25.0 - '@wordpress/data': 9.17.0(react@18.3.1) + '@wordpress/data': 9.27.0(react@18.3.1) '@wordpress/element': 5.34.0 '@wordpress/keycodes': 3.57.0 react: 18.3.1 @@ -42151,6 +42071,11 @@ snapshots: '@babel/runtime': 7.25.0 '@wordpress/i18n': 5.0.1 + '@wordpress/keycodes@4.6.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/i18n': 5.6.0 + '@wordpress/lazy-import@1.34.0': dependencies: execa: 4.1.0 @@ -42176,21 +42101,21 @@ snapshots: '@wordpress/notices@3.12.0(react@17.0.2)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@wordpress/a11y': 3.47.0 '@wordpress/data': 6.15.0(react@17.0.2) react: 17.0.2 '@wordpress/notices@3.12.0(react@18.3.1)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@wordpress/a11y': 3.47.0 '@wordpress/data': 6.15.0(react@18.3.1) react: 18.3.1 '@wordpress/notices@3.31.0(react@17.0.2)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@wordpress/a11y': 3.47.0 '@wordpress/data': 9.17.0(react@17.0.2) transitivePeerDependencies: @@ -42207,15 +42132,15 @@ snapshots: '@wordpress/notices@4.15.0(react@17.0.2)': dependencies: '@babel/runtime': 7.25.0 - '@wordpress/a11y': 3.47.0 - '@wordpress/data': 9.17.0(react@17.0.2) + '@wordpress/a11y': 3.57.0 + '@wordpress/data': 9.27.0(react@17.0.2) react: 17.0.2 '@wordpress/notices@4.15.0(react@18.3.1)': dependencies: '@babel/runtime': 7.25.0 - '@wordpress/a11y': 3.47.0 - '@wordpress/data': 9.17.0(react@18.3.1) + '@wordpress/a11y': 3.57.0 + '@wordpress/data': 9.27.0(react@18.3.1) react: 18.3.1 '@wordpress/npm-package-json-lint-config@3.1.0(npm-package-json-lint@5.4.2)': @@ -42237,7 +42162,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.34.0 - '@wordpress/html-entities': 3.47.0 + '@wordpress/html-entities': 3.57.0 '@wordpress/i18n': 4.57.0 '@wordpress/icons': 9.48.0 '@wordpress/notices': 4.15.0(react@17.0.2) @@ -42281,11 +42206,11 @@ snapshots: dependencies: '@babel/runtime': 7.25.0 '@wordpress/components': 25.16.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/compose': 6.24.0(react@17.0.2) + '@wordpress/compose': 6.34.0(react@17.0.2) '@wordpress/element': 5.34.0 '@wordpress/hooks': 3.57.0 '@wordpress/icons': 9.48.0 - '@wordpress/is-shallow-equal': 4.47.0 + '@wordpress/is-shallow-equal': 4.57.0 memize: 2.1.0 react: 17.0.2 react-dom: 17.0.2(react@17.0.2) @@ -42355,7 +42280,7 @@ snapshots: '@wordpress/preferences@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: '@babel/runtime': 7.25.0 - '@wordpress/a11y': 3.47.0 + '@wordpress/a11y': 3.57.0 '@wordpress/components': 25.16.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.34.0 @@ -42376,7 +42301,7 @@ snapshots: '@wordpress/preferences@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@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.0 - '@wordpress/a11y': 3.47.0 + '@wordpress/a11y': 3.57.0 '@wordpress/components': 25.16.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@18.3.1(react@18.3.1))(react@18.3.1) '@wordpress/data': 9.27.0(react@18.3.1) '@wordpress/element': 5.34.0 @@ -42446,6 +42371,13 @@ snapshots: '@wordpress/element': 5.34.0 classnames: 2.3.2 + '@wordpress/primitives@4.6.0(react@17.0.2)': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/element': 6.6.0 + clsx: 2.1.1 + react: 17.0.2 + '@wordpress/priority-queue@1.11.2': dependencies: '@babel/runtime': 7.25.0 @@ -42460,13 +42392,18 @@ snapshots: '@babel/runtime': 7.25.0 requestidlecallback: 0.3.0 + '@wordpress/priority-queue@3.6.0': + dependencies: + '@babel/runtime': 7.25.0 + requestidlecallback: 0.3.0 + '@wordpress/private-apis@0.20.0': dependencies: '@babel/runtime': 7.25.0 '@wordpress/private-apis@0.29.0': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@wordpress/private-apis@0.32.0': dependencies: @@ -42514,6 +42451,14 @@ snapshots: redux: 4.2.1 rungen: 0.3.2 + '@wordpress/redux-routine@5.6.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) @@ -42539,9 +42484,9 @@ snapshots: '@wordpress/blocks': 12.24.0(react@17.0.2) '@wordpress/components': 25.16.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/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.17.0(react@17.0.2) + '@wordpress/data': 9.27.0(react@17.0.2) '@wordpress/element': 5.34.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/i18n': 4.57.0 '@wordpress/icons': 9.48.0 '@wordpress/notices': 4.15.0(react@17.0.2) '@wordpress/private-apis': 0.29.0 @@ -42564,7 +42509,7 @@ snapshots: '@babel/runtime': 7.25.0 '@wordpress/compose': 4.2.0(react@18.3.1) '@wordpress/data': 5.2.0(react@18.3.1)(redux@4.2.1) - '@wordpress/dom': 3.27.0 + '@wordpress/dom': 3.6.1 '@wordpress/element': 3.2.0 '@wordpress/escape-html': 2.57.0 '@wordpress/is-shallow-equal': 4.24.0 @@ -42637,36 +42582,6 @@ snapshots: react: 18.3.1 rememo: 3.0.0 - '@wordpress/rich-text@6.24.0(react@17.0.2)': - dependencies: - '@babel/runtime': 7.25.0 - '@wordpress/a11y': 3.47.0 - '@wordpress/compose': 6.34.0(react@17.0.2) - '@wordpress/data': 9.27.0(react@17.0.2) - '@wordpress/deprecated': 3.47.0 - '@wordpress/element': 5.34.0 - '@wordpress/escape-html': 2.57.0 - '@wordpress/i18n': 4.57.0 - '@wordpress/keycodes': 3.57.0 - memize: 2.1.0 - react: 17.0.2 - rememo: 4.0.2 - - '@wordpress/rich-text@6.24.0(react@18.3.1)': - dependencies: - '@babel/runtime': 7.25.0 - '@wordpress/a11y': 3.47.0 - '@wordpress/compose': 6.34.0(react@18.3.1) - '@wordpress/data': 9.27.0(react@18.3.1) - '@wordpress/deprecated': 3.47.0 - '@wordpress/element': 5.34.0 - '@wordpress/escape-html': 2.57.0 - '@wordpress/i18n': 4.57.0 - '@wordpress/keycodes': 3.57.0 - memize: 2.1.0 - react: 18.3.1 - rememo: 4.0.2 - '@wordpress/rich-text@6.34.0(react@17.0.2)': dependencies: '@babel/runtime': 7.25.0 @@ -42695,9 +42610,23 @@ snapshots: memize: 2.1.0 react: 18.3.1 + '@wordpress/rich-text@7.6.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 + memize: 2.1.0 + react: 17.0.2 + '@wordpress/router@0.7.0(react@17.0.2)': dependencies: - '@babel/runtime': 7.23.5 + '@babel/runtime': 7.25.0 '@wordpress/element': 5.22.0 '@wordpress/private-apis': 0.20.0 '@wordpress/url': 3.48.0 @@ -43019,7 +42948,7 @@ snapshots: '@wordpress/server-side-render@3.10.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)': 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/components': 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) @@ -43084,7 +43013,7 @@ snapshots: '@wordpress/components': 25.16.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/compose': 6.34.0(react@17.0.2) '@wordpress/data': 9.27.0(react@17.0.2) - '@wordpress/deprecated': 3.47.0 + '@wordpress/deprecated': 3.57.0 '@wordpress/element': 5.34.0 '@wordpress/i18n': 4.57.0 '@wordpress/url': 3.48.0 @@ -43107,7 +43036,7 @@ snapshots: '@wordpress/style-engine@0.15.0': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 lodash: 4.17.21 '@wordpress/style-engine@0.2.0': @@ -43177,7 +43106,12 @@ snapshots: '@wordpress/undo-manager@0.7.0': dependencies: '@babel/runtime': 7.25.0 - '@wordpress/is-shallow-equal': 4.47.0 + '@wordpress/is-shallow-equal': 4.57.0 + + '@wordpress/undo-manager@1.6.0': + dependencies: + '@babel/runtime': 7.25.0 + '@wordpress/is-shallow-equal': 5.6.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: @@ -43225,7 +43159,7 @@ snapshots: '@wordpress/viewport@4.20.0(react@17.0.2)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@wordpress/compose': 5.20.0(react@17.0.2) '@wordpress/data': 7.6.0(react@17.0.2) react: 17.0.2 @@ -43241,8 +43175,8 @@ snapshots: '@wordpress/viewport@5.24.0(react@17.0.2)': dependencies: '@babel/runtime': 7.25.0 - '@wordpress/compose': 6.24.0(react@17.0.2) - '@wordpress/data': 9.17.0(react@17.0.2) + '@wordpress/compose': 6.34.0(react@17.0.2) + '@wordpress/data': 9.27.0(react@17.0.2) '@wordpress/element': 5.34.0 react: 17.0.2 @@ -43254,6 +43188,8 @@ snapshots: '@wordpress/warning@2.6.1': {} + '@wordpress/warning@3.6.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: '@babel/runtime': 7.25.0 @@ -43261,11 +43197,11 @@ snapshots: '@wordpress/block-editor': 12.15.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/blocks': 12.24.0(react@17.0.2) '@wordpress/components': 25.16.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/compose': 6.24.0(react@17.0.2) + '@wordpress/compose': 6.34.0(react@17.0.2) '@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.17.0(react@17.0.2) + '@wordpress/data': 9.27.0(react@17.0.2) '@wordpress/element': 5.34.0 - '@wordpress/i18n': 4.47.0 + '@wordpress/i18n': 4.57.0 '@wordpress/icons': 9.48.0 '@wordpress/notices': 4.15.0(react@17.0.2) classnames: 2.3.2 @@ -46817,7 +46753,7 @@ snapshots: date-fns@2.30.0: dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 date-fns@3.6.0: {} @@ -47739,7 +47675,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@9.4.0) + debug: 4.3.4(supports-color@8.1.1) 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) @@ -47756,7 +47692,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@9.4.0) + debug: 4.3.4(supports-color@8.1.1) 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) @@ -48109,7 +48045,7 @@ snapshots: eslint-plugin-jsx-a11y@6.8.0(eslint@8.55.0): dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 aria-query: 5.3.0 array-includes: 3.1.7 array.prototype.flatmap: 1.3.2 @@ -48442,7 +48378,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -49170,7 +49106,7 @@ snapshots: follow-redirects@1.15.6(debug@4.3.4): optionalDependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) follow-redirects@1.5.10: dependencies: @@ -49391,6 +49327,14 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + framer-motion@11.3.30(@emotion/is-prop-valid@1.2.1)(react-dom@17.0.2(react@17.0.2))(react@17.0.2): + dependencies: + tslib: 2.6.3 + optionalDependencies: + '@emotion/is-prop-valid': 1.2.1 + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + framer-motion@6.5.1(react-dom@17.0.2(react@17.0.2))(react@17.0.2): dependencies: '@motionone/dom': 10.12.0 @@ -52393,7 +52337,9 @@ snapshots: pretty-format: 24.9.0 throat: 4.1.0 transitivePeerDependencies: + - bufferutil - supports-color + - utf-8-validate jest-jasmine2@25.5.4: dependencies: @@ -54065,7 +54011,7 @@ snapshots: chalk: 5.2.0 cli-truncate: 3.1.0 commander: 10.0.1 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) execa: 7.2.0 lilconfig: 2.1.0 listr2: 5.0.8(enquirer@2.4.1) @@ -55369,7 +55315,7 @@ snapshots: dependencies: carlo: 0.9.46 chokidar: 3.5.3 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) isbinaryfile: 3.0.3 mime: 2.6.0 opn: 5.5.0 @@ -55897,7 +55843,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@9.4.0) + debug: 4.3.4(supports-color@8.1.1) find-yarn-workspace-root: 2.0.0 fs-extra: 8.1.0 github-slugger: 1.5.0 @@ -56525,7 +56471,7 @@ snapshots: polished@4.2.2: dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 popmotion@11.0.3: dependencies: @@ -57409,7 +57355,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@9.4.0) + debug: 4.3.4(supports-color@8.1.1) devtools-protocol: 0.0.981744 extract-zip: 2.0.1 https-proxy-agent: 5.0.1 @@ -57448,7 +57394,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@9.4.0) + debug: 4.3.4(supports-color@8.1.1) devtools-protocol: 0.0.1147663 ws: 8.13.0 optionalDependencies: @@ -57464,7 +57410,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@9.4.0) + debug: 4.3.4(supports-color@8.1.1) devtools-protocol: 0.0.1147663 ws: 8.13.0 optionalDependencies: @@ -57480,7 +57426,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@9.4.0) + debug: 4.3.4(supports-color@8.1.1) devtools-protocol: 0.0.1203626 ws: 8.14.2 transitivePeerDependencies: @@ -57516,7 +57462,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@9.4.0) + debug: 4.3.4(supports-color@8.1.1) devtools-protocol: 0.0.1036444 extract-zip: 2.0.1 https-proxy-agent: 5.0.1 @@ -57850,7 +57796,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@9.4.0) + debug: 4.3.4(supports-color@8.1.1) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -58011,7 +57957,7 @@ snapshots: react-inspector@5.1.1(react@17.0.2): dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 is-dom: 1.1.0 prop-types: 15.8.1 react: 17.0.2 @@ -58399,7 +58345,7 @@ snapshots: react-select@3.2.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2): dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 '@emotion/cache': 10.0.29 '@emotion/core': 10.3.1(react@17.0.2) '@emotion/css': 10.0.27 @@ -59806,7 +59752,7 @@ snapshots: dependencies: '@kwsites/file-exists': 1.1.1 '@kwsites/promise-deferred': 1.1.1 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -60554,7 +60500,7 @@ snapshots: colord: 2.9.3 cosmiconfig: 7.1.0 css-functions-list: 3.2.1 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) fast-glob: 3.3.2 fastest-levenshtein: 1.0.16 file-entry-cache: 6.0.1 @@ -61327,7 +61273,7 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.23.5) - ts-jest@29.1.1(@babel/core@7.24.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(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-jest@29.1.1(@babel/core@7.24.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(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): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -61859,14 +61805,14 @@ snapshots: optionalDependencies: file-loader: 6.2.0(webpack@5.89.0(webpack-cli@4.10.0)) - url-loader@4.1.1(file-loader@6.2.0(webpack@4.47.0(webpack-cli@3.3.12(webpack@5.89.0))))(webpack@4.47.0(webpack-cli@3.3.12(webpack@5.89.0))): + url-loader@4.1.1(file-loader@6.2.0(webpack@4.47.0))(webpack@4.47.0): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 - webpack: 4.47.0(webpack-cli@3.3.12(webpack@5.89.0)) + webpack: 4.47.0 optionalDependencies: - file-loader: 6.2.0(webpack@4.47.0(webpack-cli@3.3.12(webpack@5.89.0))) + file-loader: 6.2.0(webpack@4.47.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)): dependencies: @@ -61877,14 +61823,14 @@ snapshots: optionalDependencies: file-loader: 6.2.0(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))(webpack@4.47.0): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.89.0(webpack-cli@3.3.12)))(webpack@4.47.0(webpack-cli@3.3.12(webpack@5.89.0))): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 - webpack: 4.47.0 + webpack: 4.47.0(webpack-cli@3.3.12(webpack@5.89.0)) optionalDependencies: - file-loader: 6.2.0(webpack@4.47.0) + file-loader: 6.2.0(webpack@4.47.0(webpack-cli@3.3.12(webpack@5.89.0))) url-loader@4.1.1(file-loader@6.2.0(webpack@5.91.0(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@5.1.4)))(webpack@5.91.0(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@4.10.0)): dependencies: From dec89a13111f5c8cf2bc6864677b63453bc0ef35 Mon Sep 17 00:00:00 2001 From: Seghir Nadir Date: Fri, 30 Aug 2024 18:24:53 +0100 Subject: [PATCH 005/187] Revert "Store API: Do not resume orders with `pending` status (#50531)" (#51067) * Revert "Store API: Do not resume orders with `pending` status (#50531)" This reverts commit 3170acd1b0d15338e14079e464d1eabf64096751. * Add changefile(s) from automation for the following project(s): woocommerce --------- Co-authored-by: github-actions --- plugins/woocommerce/changelog/51067-revert-50531 | 4 ++++ .../woocommerce/src/StoreApi/Utilities/DraftOrderTrait.php | 5 ++--- 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 plugins/woocommerce/changelog/51067-revert-50531 diff --git a/plugins/woocommerce/changelog/51067-revert-50531 b/plugins/woocommerce/changelog/51067-revert-50531 new file mode 100644 index 00000000000..e65258f8afe --- /dev/null +++ b/plugins/woocommerce/changelog/51067-revert-50531 @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak +Comment: This reverts an existing PR. + diff --git a/plugins/woocommerce/src/StoreApi/Utilities/DraftOrderTrait.php b/plugins/woocommerce/src/StoreApi/Utilities/DraftOrderTrait.php index 4e80f2695f8..dedc60ae418 100644 --- a/plugins/woocommerce/src/StoreApi/Utilities/DraftOrderTrait.php +++ b/plugins/woocommerce/src/StoreApi/Utilities/DraftOrderTrait.php @@ -60,9 +60,8 @@ trait DraftOrderTrait { return true; } - // Failed orders and those needing payment can be retried if the cart hasn't changed. - // Pending orders are excluded from this check since they may be awaiting an update from the payment processor. - if ( $order_object->needs_payment() && ! $order_object->has_status( 'pending' ) && $order_object->has_cart_hash( wc()->cart->get_cart_hash() ) ) { + // Pending and failed orders can be retried if the cart hasn't changed. + if ( $order_object->needs_payment() && $order_object->has_cart_hash( wc()->cart->get_cart_hash() ) ) { return true; } From 3def18623e9b2a8df170000f6f3dbfb07b8caab7 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Fri, 30 Aug 2024 17:47:20 -0300 Subject: [PATCH 006/187] Fix variation selector display issues on the front end (#51023) * Add woocommerce class to single-product * Add changelog * Fix chevron alignment --- .../assets/js/blocks/single-product/save.tsx | 4 +++- .../changelog/fix-47794_variation_selector_display | 4 ++++ .../woocommerce/client/legacy/css/woocommerce.scss | 12 ++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-47794_variation_selector_display diff --git a/plugins/woocommerce-blocks/assets/js/blocks/single-product/save.tsx b/plugins/woocommerce-blocks/assets/js/blocks/single-product/save.tsx index 0feb6d8f950..4c8f4ef2973 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/single-product/save.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/single-product/save.tsx @@ -4,7 +4,9 @@ import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; const Save = () => { - const blockProps = useBlockProps.save(); + const blockProps = useBlockProps.save( { + className: 'woocommerce', + } ); return (
diff --git a/plugins/woocommerce/changelog/fix-47794_variation_selector_display b/plugins/woocommerce/changelog/fix-47794_variation_selector_display new file mode 100644 index 00000000000..84ebab4b1c5 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-47794_variation_selector_display @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix variation selector display issues on the front end #51023 diff --git a/plugins/woocommerce/client/legacy/css/woocommerce.scss b/plugins/woocommerce/client/legacy/css/woocommerce.scss index efd786f4d58..8c97759bf97 100644 --- a/plugins/woocommerce/client/legacy/css/woocommerce.scss +++ b/plugins/woocommerce/client/legacy/css/woocommerce.scss @@ -445,6 +445,18 @@ p.demo_store, min-width: 75%; display: inline-block; margin-right: 1em; + + /* We hide the default chevron because it cannot be directly modified. Instead, we add a custom chevron using a background image. */ + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + padding-right: 2em; + background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItY2hldnJvbi1kb3duIj48cG9seWxpbmUgcG9pbnRzPSI2IDkgMTIgMTUgMTggOSI+PC9wb2x5bGluZT48L3N2Zz4=) + no-repeat; + background-size: 16px; + -webkit-background-size: 16px; + background-position: calc(100% - 12px) 50%; + -webkit-background-position: calc(100% - 12px) 50%; } td.label { From 36ede651db8382c89abec14a4e5c31ac22de159a Mon Sep 17 00:00:00 2001 From: RJ <27843274+rjchow@users.noreply.github.com> Date: Mon, 2 Sep 2024 12:38:22 +0800 Subject: [PATCH 007/187] add: sanitise query params in remote logging (PHP) (#51013) --- .../fix-remote-logger-sanitise-query-params | 4 + .../src/Internal/Logging/RemoteLogger.php | 50 +- .../src/Internal/Logging/RemoteLoggerTest.php | 1051 +++++++++-------- 3 files changed, 628 insertions(+), 477 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-remote-logger-sanitise-query-params diff --git a/plugins/woocommerce/changelog/fix-remote-logger-sanitise-query-params b/plugins/woocommerce/changelog/fix-remote-logger-sanitise-query-params new file mode 100644 index 00000000000..bf2342b18e0 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-remote-logger-sanitise-query-params @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Add query params masking to remote logger diff --git a/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php b/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php index 3bcb44f4c29..6daa928b46e 100644 --- a/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php +++ b/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php @@ -70,7 +70,7 @@ class RemoteLogger extends \WC_Log_Handler { 'wc_version' => WC()->version, 'php_version' => phpversion(), 'wp_version' => get_bloginfo( 'version' ), - 'request_uri' => filter_input( INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_URL ), + 'request_uri' => $this->sanitize_request_uri( filter_input( INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_URL ) ), ), ); @@ -431,4 +431,52 @@ class RemoteLogger extends \WC_Log_Handler { protected function is_dev_or_local_environment() { return in_array( wp_get_environment_type(), array( 'development', 'local' ), true ); } + /** + * Sanitize the request URI to only allow certain query parameters. + * + * @param string $request_uri The request URI to sanitize. + * @return string The sanitized request URI. + */ + private function sanitize_request_uri( $request_uri ) { + $default_whitelist = array( 'path', 'page', 'step', 'task', 'tab' ); + + /** + * Filter to allow other plugins to whitelist request_uri query parameter values for unmasked remote logging. + * + * @since 9.4.0 + * + * @param string $default_whitelist The default whitelist of query parameters. + */ + $whitelist = apply_filters( 'woocommerce_remote_logger_request_uri_whitelist', $default_whitelist ); + + $parsed_url = wp_parse_url( $request_uri ); + if ( ! isset( $parsed_url['query'] ) ) { + return $request_uri; + } + + parse_str( $parsed_url['query'], $query_params ); + + foreach ( $query_params as $key => &$value ) { + if ( ! in_array( $key, $whitelist, true ) ) { + $value = 'xxxxxx'; + } + } + + $parsed_url['query'] = http_build_query( $query_params ); + return $this->build_url( $parsed_url ); + } + + /** + * Build a URL from its parsed components. + * + * @param array $parsed_url The parsed URL components. + * @return string The built URL. + */ + private function build_url( $parsed_url ) { + $path = $parsed_url['path'] ?? ''; + $query = isset( $parsed_url['query'] ) ? "?{$parsed_url['query']}" : ''; + $fragment = isset( $parsed_url['fragment'] ) ? "#{$parsed_url['fragment']}" : ''; + + return "$path$query$fragment"; + } } diff --git a/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php b/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php index 63327438a03..1133880bad2 100644 --- a/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php +++ b/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php @@ -1,547 +1,646 @@ sut = wc_get_container()->get( RemoteLogger::class ); - } + class RemoteLoggerTest extends \WC_Unit_Test_Case { + /** + * System under test. + * + * @var RemoteLogger + */ + private $sut; - /** - * Tear down. - * - * @return void - */ - public function tearDown(): void { - $this->cleanup_filters(); - delete_option( 'woocommerce_feature_remote_logging_enabled' ); - delete_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT ); - global $wpdb; - $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_rate_limits" ); - WC_Cache_Helper::invalidate_cache_group( WC_Rate_Limiter::CACHE_GROUP ); - } - - /** - * Cleanup filters used in tests. - * - * @return void - */ - private function cleanup_filters() { - $filters = array( - 'option_woocommerce_admin_remote_feature_enabled', - 'option_woocommerce_allow_tracking', - 'option_woocommerce_version', - 'option_woocommerce_remote_variant_assignment', - 'plugins_api', - 'pre_http_request', - 'woocommerce_remote_logger_formatted_log_data', - 'pre_site_transient_update_plugins', - ); - foreach ( $filters as $filter ) { - remove_all_filters( $filter ); + /** + * Set up test + * + * @return void + */ + public function setUp(): void { + parent::setUp(); + $this->sut = wc_get_container()->get( RemoteLogger::class ); } - } - /** - * @testdox Remote logging is allowed when all conditions are met - */ - public function test_remote_logging_allowed() { - $this->setup_remote_logging_conditions( true ); - $this->assertTrue( $this->sut->is_remote_logging_allowed() ); - } + /** + * Tear down. + * + * @return void + */ + public function tearDown(): void { + $this->cleanup_filters(); + delete_option( 'woocommerce_feature_remote_logging_enabled' ); + delete_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT ); + global $wpdb; + $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_rate_limits" ); + WC_Cache_Helper::invalidate_cache_group( WC_Rate_Limiter::CACHE_GROUP ); + } - /** - * @testdox Remote logging is not allowed under various conditions - * @dataProvider remote_logging_disallowed_provider - * - * @param string $condition The condition being tested. - * @param callable $setup_callback Callback to set up the test condition. - */ - public function test_remote_logging_not_allowed( $condition, $setup_callback ) { - $this->setup_remote_logging_conditions( true ); - $setup_callback( $this ); - $this->assertFalse( $this->sut->is_remote_logging_allowed() ); - } + /** + * Cleanup filters used in tests. + * + * @return void + */ + private function cleanup_filters() { + $filters = array( + 'option_woocommerce_admin_remote_feature_enabled', + 'option_woocommerce_allow_tracking', + 'option_woocommerce_version', + 'option_woocommerce_remote_variant_assignment', + 'plugins_api', + 'pre_http_request', + 'woocommerce_remote_logger_formatted_log_data', + 'pre_site_transient_update_plugins', + 'woocommerce_remote_logger_request_uri_whitelist', + ); + foreach ( $filters as $filter ) { + remove_all_filters( $filter ); + } + } - /** - * Data provider for test_remote_logging_not_allowed. - * - * @return array[] Test cases with conditions and setup callbacks. - */ - public function remote_logging_disallowed_provider() { - return array( - 'feature flag disabled' => array( - 'condition' => 'feature flag disabled', - 'setup' => fn() => update_option( 'woocommerce_feature_remote_logging_enabled', 'no' ), - ), - 'tracking opted out' => array( - 'condition' => 'tracking opted out', - 'setup' => fn() => add_filter( 'option_woocommerce_allow_tracking', fn() => 'no' ), - ), - 'high variant assignment' => array( - 'condition' => 'high variant assignment', - 'setup' => fn() => add_filter( 'option_woocommerce_remote_variant_assignment', fn() => 15 ), - ), - 'outdated version' => array( - 'condition' => 'outdated version', - 'setup' => function () { - $version = WC()->version; - // Next major version. (e.g. 9.0.1 -> 10.0.0). - $next_version = implode( - '.', - array_map( - function ( $n, $i ) { - return 0 === $i ? $n + 1 : 0; - }, - explode( '.', $version ), - array_keys( explode( '.', $version ) ) - ) - ); + /** + * @testdox Remote logging is allowed when all conditions are met + */ + public function test_remote_logging_allowed() { + $this->setup_remote_logging_conditions( true ); + $this->assertTrue( $this->sut->is_remote_logging_allowed() ); + } - set_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT, $next_version, WEEK_IN_SECONDS ); - }, - ), - ); - } + /** + * @testdox Remote logging is not allowed under various conditions + * @dataProvider remote_logging_disallowed_provider + * + * @param string $condition The condition being tested. + * @param callable $setup_callback Callback to set up the test condition. + */ + public function test_remote_logging_not_allowed( $condition, $setup_callback ) { + $this->setup_remote_logging_conditions( true ); + $setup_callback( $this ); + $this->assertFalse( $this->sut->is_remote_logging_allowed() ); + } + /** + * Data provider for test_remote_logging_not_allowed. + * + * @return array[] Test cases with conditions and setup callbacks. + */ + public function remote_logging_disallowed_provider() { + return array( + 'feature flag disabled' => array( + 'condition' => 'feature flag disabled', + 'setup' => fn() => update_option( 'woocommerce_feature_remote_logging_enabled', 'no' ), + ), + 'tracking opted out' => array( + 'condition' => 'tracking opted out', + 'setup' => fn() => add_filter( 'option_woocommerce_allow_tracking', fn() => 'no' ), + ), + 'high variant assignment' => array( + 'condition' => 'high variant assignment', + 'setup' => fn() => add_filter( 'option_woocommerce_remote_variant_assignment', fn() => 15 ), + ), + 'outdated version' => array( + 'condition' => 'outdated version', + 'setup' => function () { + $version = WC()->version; + // Next major version. (e.g. 9.0.1 -> 10.0.0). + $next_version = implode( + '.', + array_map( + function ( $n, $i ) { + return 0 === $i ? $n + 1 : 0; + }, + explode( '.', $version ), + array_keys( explode( '.', $version ) ) + ) + ); - /** - * @testdox should_current_version_be_logged method behaves correctly - * @dataProvider should_current_version_be_logged_provider - * - * @param string $current_version The current WooCommerce version. - * @param string $new_version The new WooCommerce version. - * @param string $transient_value The value of the transient. - * @param bool $expected The expected result. - */ - public function test_should_current_version_be_logged( $current_version, $new_version, $transient_value, $expected ) { - $wc_version = WC()->version; - WC()->version = $current_version; + set_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT, $next_version, WEEK_IN_SECONDS ); + }, + ), + ); + } - // Set up the transient. - if ( null !== $transient_value ) { - set_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT, $transient_value, WEEK_IN_SECONDS ); - } else { + /** + * @testdox should_current_version_be_logged method behaves correctly + * @dataProvider should_current_version_be_logged_provider + * + * @param string $current_version The current WooCommerce version. + * @param string $new_version The new WooCommerce version. + * @param string $transient_value The value of the transient. + * @param bool $expected The expected result. + */ + public function test_should_current_version_be_logged( $current_version, $new_version, $transient_value, $expected ) { + $wc_version = WC()->version; + WC()->version = $current_version; + + // Set up the transient. + if ( null !== $transient_value ) { + set_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT, $transient_value, WEEK_IN_SECONDS ); + } else { + delete_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT ); + + $this->setup_mock_plugin_updates( $new_version ); + } + + $result = $this->invoke_private_method( $this->sut, 'should_current_version_be_logged', array() ); + $this->assertEquals( $expected, $result ); + + // Clean up. delete_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT ); - $this->setup_mock_plugin_updates( $new_version ); + WC()->version = $wc_version; } - $result = $this->invoke_private_method( $this->sut, 'should_current_version_be_logged', array() ); - $this->assertEquals( $expected, $result ); - - // Clean up. - delete_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT ); - - WC()->version = $wc_version; - } - - /** - * Data provider for test_should_current_version_be_logged. - */ - public function should_current_version_be_logged_provider() { - return array( - 'current version is latest (transient set)' => array( '9.2.0', '9.2.0', '9.2.0', true ), - 'current version is newer (transient set)' => array( '9.3.0', '9.2.0', '9.2.0', true ), - 'current version is older (transient set)' => array( '9.1.0', '9.2.0', '9.2.0', false ), - 'new version is null (transient set)' => array( '9.2.0', null, null, true ), - 'transient not set, current version is latest' => array( '9.2.0', '9.2.0', null, true ), - 'transient not set, current version is newer' => array( '9.3.0', '9.2.0', null, true ), - 'transient not set, current version is older' => array( '9.1.0', '9.2.0', null, false ), - 'transient not set, new version is null' => array( '9.2.0', null, null, true ), - ); - } - - /** - * @testdox fetch_new_woocommerce_version method returns correct version - */ - public function test_fetch_new_woocommerce_version() { - $this->setup_mock_plugin_updates( '9.3.0' ); - - $result = $this->invoke_private_method( $this->sut, 'fetch_new_woocommerce_version', array() ); - $this->assertEquals( '9.3.0', $result, 'The result should be the latest version when an update is available.' ); - } - - /** - * @testdox fetch_new_woocommerce_version method returns null when no update is available - */ - public function test_fetch_new_woocommerce_version_no_update() { - add_filter( 'pre_site_transient_update_plugins', fn() => array() ); - - $result = $this->invoke_private_method( $this->sut, 'fetch_new_woocommerce_version', array() ); - $this->assertNull( $result, 'The result should be null when no update is available.' ); - } - - /** - * @testdox get_formatted_log method returns expected array structure - * @dataProvider get_formatted_log_provider - * - * @param string $level The log level. - * @param string $message The log message. - * @param array $context The log context. - * @param array $expected The expected formatted log array. - */ - public function test_get_formatted_log( $level, $message, $context, $expected ) { - $formatted_log = $this->sut->get_formatted_log( $level, $message, $context ); - foreach ( $expected as $key => $value ) { - $this->assertArrayHasKey( $key, $formatted_log ); - $this->assertEquals( $value, $formatted_log[ $key ] ); + /** + * Data provider for test_should_current_version_be_logged. + */ + public function should_current_version_be_logged_provider() { + return array( + 'current version is latest (transient set)' => array( '9.2.0', '9.2.0', '9.2.0', true ), + 'current version is newer (transient set)' => array( '9.3.0', '9.2.0', '9.2.0', true ), + 'current version is older (transient set)' => array( '9.1.0', '9.2.0', '9.2.0', false ), + 'new version is null (transient set)' => array( '9.2.0', null, null, true ), + 'transient not set, current version is latest' => array( '9.2.0', '9.2.0', null, true ), + 'transient not set, current version is newer' => array( '9.3.0', '9.2.0', null, true ), + 'transient not set, current version is older' => array( '9.1.0', '9.2.0', null, false ), + 'transient not set, new version is null' => array( '9.2.0', null, null, true ), + ); } - } - /** - * Data provider for test_get_formatted_log. - * - * @return array[] Test cases with log data and expected formatted output. - */ - public function get_formatted_log_provider() { - return array( - 'basic log data' => array( - 'error', - 'Fatal error occurred at line 123 in ' . ABSPATH . 'wp-content/file.php', - array( 'tags' => array( 'tag1', 'tag2' ) ), - array( - 'feature' => 'woocommerce_core', - 'severity' => 'error', - 'message' => 'Fatal error occurred at line 123 in **/wp-content/file.php', - 'tags' => array( 'woocommerce', 'php', 'tag1', 'tag2' ), - ), - ), - 'log with backtrace' => array( - 'error', - 'Test error message', - array( 'backtrace' => ABSPATH . 'wp-content/plugins/woocommerce/file.php' ), - array( 'trace' => '**/woocommerce/file.php' ), - ), - 'log with extra attributes' => array( - 'error', - 'Test error message', - array( - 'extra' => array( - 'key1' => 'value1', - 'key2' => 'value2', + /** + * @testdox fetch_new_woocommerce_version method returns correct version + */ + public function test_fetch_new_woocommerce_version() { + $this->setup_mock_plugin_updates( '9.3.0' ); + + $result = $this->invoke_private_method( $this->sut, 'fetch_new_woocommerce_version', array() ); + $this->assertEquals( '9.3.0', $result, 'The result should be the latest version when an update is available.' ); + } + + /** + * @testdox fetch_new_woocommerce_version method returns null when no update is available + */ + public function test_fetch_new_woocommerce_version_no_update() { + add_filter( 'pre_site_transient_update_plugins', fn() => array() ); + + $result = $this->invoke_private_method( $this->sut, 'fetch_new_woocommerce_version', array() ); + $this->assertNull( $result, 'The result should be null when no update is available.' ); + } + + + /** + * @testdox get_formatted_log method returns expected array structure + * @dataProvider get_formatted_log_provider + * + * @param string $level The log level. + * @param string $message The log message. + * @param array $context The log context. + * @param array $expected The expected formatted log array. + */ + public function test_get_formatted_log( $level, $message, $context, $expected ) { + $formatted_log = $this->sut->get_formatted_log( $level, $message, $context ); + foreach ( $expected as $key => $value ) { + $this->assertArrayHasKey( $key, $formatted_log ); + $this->assertEquals( $value, $formatted_log[ $key ] ); + } + } + + /** + * Data provider for test_get_formatted_log. + * + * @return array[] Test cases with log data and expected formatted output. + */ + public function get_formatted_log_provider() { + return array( + 'basic log data' => array( + 'error', + 'Fatal error occurred at line 123 in ' . ABSPATH . 'wp-content/file.php', + array( 'tags' => array( 'tag1', 'tag2' ) ), + array( + 'feature' => 'woocommerce_core', + 'severity' => 'error', + 'message' => 'Fatal error occurred at line 123 in **/wp-content/file.php', + 'tags' => array( 'woocommerce', 'php', 'tag1', 'tag2' ), ), ), - array( - 'extra' => array( - 'key1' => 'value1', - 'key2' => 'value2', + 'log with backtrace' => array( + 'error', + 'Test error message', + array( 'backtrace' => ABSPATH . 'wp-content/plugins/woocommerce/file.php' ), + array( 'trace' => '**/woocommerce/file.php' ), + ), + 'log with extra attributes' => array( + 'error', + 'Test error message', + array( + 'extra' => array( + 'key1' => 'value1', + 'key2' => 'value2', + ), + ), + array( + 'extra' => array( + 'key1' => 'value1', + 'key2' => 'value2', + ), ), ), - ), - ); - } + ); + } - /** - * @testdox should_handle method behaves correctly under different conditions - * @dataProvider should_handle_provider - * - * @param callable $setup Function to set up the test environment. - * @param string $level Log level to test. - * @param bool $expected Expected result of should_handle method. - */ - public function test_should_handle( $setup, $level, $expected ) { - $this->sut = $this->getMockBuilder( RemoteLogger::class ) + + /** + * @testdox get_formatted_log method correctly sanitizes request URI + */ + public function test_get_formatted_log_sanitizes_request_uri() { + global $mock_filter_input, $mock_return; + $mock_filter_input = true; + $mock_return = '/shop?path=home&user=admin&token=abc123'; + + $formatted_log = $this->sut->get_formatted_log( 'error', 'Test message', array() ); + + $mock_filter_input = false; + + $this->assertArrayHasKey( 'properties', $formatted_log ); + $this->assertArrayHasKey( 'request_uri', $formatted_log['properties'] ); + $this->assertNotNull( $formatted_log['properties']['request_uri'], 'Request URI should not be null' ); + $this->assertStringContainsString( 'path=home', $formatted_log['properties']['request_uri'] ); + $this->assertStringContainsString( 'user=xxxxxx', $formatted_log['properties']['request_uri'] ); + $this->assertStringContainsString( 'token=xxxxxx', $formatted_log['properties']['request_uri'] ); + } + + /** + * @testdox sanitize_request_uri method respects whitelist filter + */ + public function test_sanitize_request_uri_respects_whitelist_filter() { + add_filter( + 'woocommerce_remote_logger_request_uri_whitelist', + function ( $whitelist ) { + $whitelist[] = 'custom_param'; + return $whitelist; + } + ); + + $request_uri = '/shop?path=home&custom_param=value&token=abc123'; + $sanitized_uri = $this->invoke_private_method( $this->sut, 'sanitize_request_uri', array( $request_uri ) ); + $this->assertStringContainsString( 'path=home', $sanitized_uri ); + $this->assertStringContainsString( 'custom_param=value', $sanitized_uri ); + $this->assertStringContainsString( 'token=xxxxxx', $sanitized_uri ); + } + + /** + * @testdox sanitize_request_uri method correctly sanitizes request URIs + */ + public function test_sanitize_request_uri() { + $reflection = new \ReflectionClass( $this->sut ); + $method = $reflection->getMethod( 'sanitize_request_uri' ); + $method->setAccessible( true ); + + // Test with whitelisted parameters. + $request_uri = '/shop?path=home&page=2&step=1&task=checkout'; + $sanitized_uri = $method->invokeArgs( $this->sut, array( $request_uri ) ); + $this->assertStringContainsString( 'path=home', $sanitized_uri ); + $this->assertStringContainsString( 'page=2', $sanitized_uri ); + $this->assertStringContainsString( 'step=1', $sanitized_uri ); + $this->assertStringContainsString( 'task=checkout', $sanitized_uri ); + + // Test with non-whitelisted parameters. + $request_uri = '/shop?path=home&user=admin&token=abc123'; + $sanitized_uri = $method->invokeArgs( $this->sut, array( $request_uri ) ); + $this->assertStringContainsString( 'path=home', $sanitized_uri ); + $this->assertStringContainsString( 'user=xxxxxx', $sanitized_uri ); + $this->assertStringContainsString( 'token=xxxxxx', $sanitized_uri ); + + // Test with mixed parameters. + $request_uri = '/shop?path=home&page=2&user=admin&step=1&token=abc123'; + $sanitized_uri = $method->invokeArgs( $this->sut, array( $request_uri ) ); + $this->assertStringContainsString( 'path=home', $sanitized_uri ); + $this->assertStringContainsString( 'page=2', $sanitized_uri ); + $this->assertStringContainsString( 'step=1', $sanitized_uri ); + $this->assertStringContainsString( 'user=xxxxxx', $sanitized_uri ); + $this->assertStringContainsString( 'token=xxxxxx', $sanitized_uri ); + } + + /** + * @testdox should_handle method behaves correctly under different conditions + * @dataProvider should_handle_provider + * + * @param callable $setup Function to set up the test environment. + * @param string $level Log level to test. + * @param bool $expected Expected result of should_handle method. + */ + public function test_should_handle( $setup, $level, $expected ) { + $this->sut = $this->getMockBuilder( RemoteLogger::class ) ->onlyMethods( array( 'is_remote_logging_allowed', 'is_third_party_error' ) ) ->getMock(); - $this->sut->method( 'is_remote_logging_allowed' )->willReturn( true ); - $this->sut->method( 'is_third_party_error' )->willReturn( false ); + $this->sut->method( 'is_remote_logging_allowed' )->willReturn( true ); + $this->sut->method( 'is_third_party_error' )->willReturn( false ); - $setup( $this ); + $setup( $this ); - $result = $this->invoke_private_method( $this->sut, 'should_handle', array( $level, 'Test message', array() ) ); - $this->assertEquals( $expected, $result ); - } + $result = $this->invoke_private_method( $this->sut, 'should_handle', array( $level, 'Test message', array() ) ); + $this->assertEquals( $expected, $result ); + } - /** - * Data provider for test_should_handle method. - * - * @return array Test cases for should_handle method. - */ - public function should_handle_provider() { - return array( - 'throttled' => array( - fn() => WC_Rate_Limiter::set_rate_limit( RemoteLogger::RATE_LIMIT_ID, 10 ), - 'critical', - false, - ), - 'less severe than critical' => array( - fn() => null, - 'error', - false, - ), - 'critical level' => array( - fn() => null, - 'critical', - true, - ), - ); - } + /** + * Data provider for test_should_handle method. + * + * @return array Test cases for should_handle method. + */ + public function should_handle_provider() { + return array( + 'throttled' => array( + fn() => WC_Rate_Limiter::set_rate_limit( RemoteLogger::RATE_LIMIT_ID, 10 ), + 'critical', + false, + ), + 'less severe than critical' => array( + fn() => null, + 'error', + false, + ), + 'critical level' => array( + fn() => null, + 'critical', + true, + ), + ); + } - /** - * @testdox handle method applies filter and doesn't send logs when filtered to null - */ - public function test_handle_filtered_log_null() { - $this->sut = $this->getMockBuilder( RemoteLogger::class ) + /** + * @testdox handle method applies filter and doesn't send logs when filtered to null + */ + public function test_handle_filtered_log_null() { + $this->sut = $this->getMockBuilder( RemoteLogger::class ) ->onlyMethods( array( 'is_remote_logging_allowed' ) ) ->getMock(); - $this->sut->method( 'is_remote_logging_allowed' )->willReturn( true ); + $this->sut->method( 'is_remote_logging_allowed' )->willReturn( true ); - add_filter( 'woocommerce_remote_logger_formatted_log_data', fn() => null, 10, 4 ); - add_filter( 'pre_http_request', fn() => $this->fail( 'wp_safe_remote_post should not be called' ), 10, 3 ); + add_filter( 'woocommerce_remote_logger_formatted_log_data', fn() => null, 10, 4 ); + add_filter( 'pre_http_request', fn() => $this->fail( 'wp_safe_remote_post should not be called' ), 10, 3 ); - $this->assertFalse( $this->sut->handle( time(), 'error', 'Test message', array() ) ); - } + $this->assertFalse( $this->sut->handle( time(), 'error', 'Test message', array() ) ); + } - /** - * @testdox handle method does not send logs in dev environment - */ - public function test_handle_does_not_send_logs_in_dev_environment() { - $this->sut = $this->getMockBuilder( RemoteLoggerWithEnvironmentOverride::class ) + /** + * @testdox handle method does not send logs in dev environment + */ + public function test_handle_does_not_send_logs_in_dev_environment() { + $this->sut = $this->getMockBuilder( RemoteLoggerWithEnvironmentOverride::class ) ->onlyMethods( array( 'is_remote_logging_allowed' ) ) ->getMock(); - $this->sut->set_is_dev_or_local( true ); - $this->sut->method( 'is_remote_logging_allowed' )->willReturn( true ); + $this->sut->set_is_dev_or_local( true ); + $this->sut->method( 'is_remote_logging_allowed' )->willReturn( true ); - $this->assertFalse( $this->sut->handle( time(), 'error', 'Test message', array() ) ); - } + $this->assertFalse( $this->sut->handle( time(), 'error', 'Test message', array() ) ); + } - /** - * @testdox handle method successfully sends log - */ - public function test_handle_successful() { - $this->sut = $this->getMockBuilder( RemoteLoggerWithEnvironmentOverride::class ) + /** + * @testdox handle method successfully sends log + */ + public function test_handle_successful() { + $this->sut = $this->getMockBuilder( RemoteLoggerWithEnvironmentOverride::class ) ->onlyMethods( array( 'is_remote_logging_allowed' ) ) ->getMock(); - $this->sut->set_is_dev_or_local( false ); - $this->sut->method( 'is_remote_logging_allowed' )->willReturn( true ); + $this->sut->set_is_dev_or_local( false ); + $this->sut->method( 'is_remote_logging_allowed' )->willReturn( true ); - add_filter( - 'pre_http_request', - function ( $preempt, $args ) { - $this->assertArrayHasKey( 'body', $args ); - $this->assertArrayHasKey( 'headers', $args ); - return array( - 'response' => array( - 'code' => 200, - 'message' => 'OK', + add_filter( + 'pre_http_request', + function ( $preempt, $args ) { + $this->assertArrayHasKey( 'body', $args ); + $this->assertArrayHasKey( 'headers', $args ); + return array( + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'body' => wp_json_encode( array( 'success' => true ) ), + ); + }, + 10, + 3 + ); + + $this->assertTrue( $this->sut->handle( time(), 'critical', 'Test message', array() ) ); + $this->assertTrue( WC_Rate_Limiter::retried_too_soon( RemoteLogger::RATE_LIMIT_ID ) ); + } + + /** + * @testdox handle method handles remote logging failure + */ + public function test_handle_remote_logging_failure() { + $this->sut = $this->getMockBuilder( RemoteLoggerWithEnvironmentOverride::class ) + ->onlyMethods( array( 'is_remote_logging_allowed' ) ) + ->getMock(); + + $this->sut->set_is_dev_or_local( false ); + $this->sut->method( 'is_remote_logging_allowed' )->willReturn( true ); + + add_filter( + 'pre_http_request', + function ( $preempt, $args, $url ) { + if ( 'https://public-api.wordpress.com/rest/v1.1/logstash' === $url ) { + throw new \Exception( 'Remote logging failed: A valid URL was not provided.' ); + } + return $preempt; + }, + 10, + 3 + ); + + $this->assertFalse( $this->sut->handle( time(), 'critical', 'Test message', array() ) ); + $this->assertTrue( WC_Rate_Limiter::retried_too_soon( RemoteLogger::RATE_LIMIT_ID ) ); + } + + /** + * @testdox is_third_party_error method correctly identifies third-party errors + * @dataProvider is_third_party_error_provider + * @param string $message The error message to check. + * @param array $context The context of the error. + * @param bool $expected_result The expected result of the check. + */ + public function test_is_third_party_error( $message, $context, $expected_result ) { + $result = $this->invoke_private_method( $this->sut, 'is_third_party_error', array( $message, $context ) ); + $this->assertEquals( $expected_result, $result ); + } + + /** + * Data provider for test_is_third_party_error. + * + * @return array[] Test cases. + */ + public function is_third_party_error_provider() { + return array( + array( 'Fatal error in ' . WC_ABSPATH . 'file.php', array(), false ), + array( 'Fatal error in /wp-content/file.php', array(), false ), + array( 'Fatal error in /wp-content/file.php', array( 'source' => 'fatal-errors' ), false ), + array( + 'Fatal error in /wp-content/plugins/3rd-plugin/file.php', + array( + 'source' => 'fatal-errors', + 'backtrace' => array( '/wp-content/plugins/3rd-plugin/file.php', WC_ABSPATH . 'file.php' ), ), - 'body' => wp_json_encode( array( 'success' => true ) ), - ); - }, - 10, - 3 - ); - - $this->assertTrue( $this->sut->handle( time(), 'critical', 'Test message', array() ) ); - $this->assertTrue( WC_Rate_Limiter::retried_too_soon( RemoteLogger::RATE_LIMIT_ID ) ); - } - - /** - * @testdox handle method handles remote logging failure - */ - public function test_handle_remote_logging_failure() { - $this->sut = $this->getMockBuilder( RemoteLoggerWithEnvironmentOverride::class ) - ->onlyMethods( array( 'is_remote_logging_allowed' ) ) - ->getMock(); - - $this->sut->set_is_dev_or_local( false ); - $this->sut->method( 'is_remote_logging_allowed' )->willReturn( true ); - - add_filter( - 'pre_http_request', - function ( $preempt, $args, $url ) { - if ( 'https://public-api.wordpress.com/rest/v1.1/logstash' === $url ) { - throw new \Exception( 'Remote logging failed: A valid URL was not provided.' ); - } - return $preempt; - }, - 10, - 3 - ); - - $this->assertFalse( $this->sut->handle( time(), 'critical', 'Test message', array() ) ); - $this->assertTrue( WC_Rate_Limiter::retried_too_soon( RemoteLogger::RATE_LIMIT_ID ) ); - } - - /** - * @testdox is_third_party_error method correctly identifies third-party errors - * @dataProvider is_third_party_error_provider - * @param string $message The error message to check. - * @param array $context The context of the error. - * @param bool $expected_result The expected result of the check. - */ - public function test_is_third_party_error( $message, $context, $expected_result ) { - $result = $this->invoke_private_method( $this->sut, 'is_third_party_error', array( $message, $context ) ); - $this->assertEquals( $expected_result, $result ); - } - - /** - * Data provider for test_is_third_party_error. - * - * @return array[] Test cases. - */ - public function is_third_party_error_provider() { - return array( - array( 'Fatal error in ' . WC_ABSPATH . 'file.php', array(), false ), - array( 'Fatal error in /wp-content/file.php', array(), false ), - array( 'Fatal error in /wp-content/file.php', array( 'source' => 'fatal-errors' ), false ), - array( - 'Fatal error in /wp-content/plugins/3rd-plugin/file.php', + false, + ), array( - 'source' => 'fatal-errors', - 'backtrace' => array( '/wp-content/plugins/3rd-plugin/file.php', WC_ABSPATH . 'file.php' ), + 'Fatal error in /wp-content/plugins/woocommerce-3rd-plugin/file.php', + array( + 'source' => 'fatal-errors', + 'backtrace' => array( WP_PLUGIN_DIR . 'woocommerce-3rd-plugin/file.php' ), + ), + true, ), - false, - ), - array( - 'Fatal error in /wp-content/plugins/woocommerce-3rd-plugin/file.php', array( - 'source' => 'fatal-errors', - 'backtrace' => array( WP_PLUGIN_DIR . 'woocommerce-3rd-plugin/file.php' ), + 'Fatal error in /wp-content/plugins/3rd-plugin/file.php', + array( + 'source' => 'fatal-errors', + 'backtrace' => array( WP_PLUGIN_DIR . '3rd-plugin/file.php' ), + ), + true, ), - true, - ), - array( - 'Fatal error in /wp-content/plugins/3rd-plugin/file.php', array( - 'source' => 'fatal-errors', - 'backtrace' => array( WP_PLUGIN_DIR . '3rd-plugin/file.php' ), + 'Fatal error in /wp-content/plugins/3rd-plugin/file.php', + array( + 'source' => 'fatal-errors', + 'backtrace' => array( array( 'file' => WP_PLUGIN_DIR . '3rd-plugin/file.php' ) ), + ), + true, ), - true, - ), - array( - 'Fatal error in /wp-content/plugins/3rd-plugin/file.php', - array( - 'source' => 'fatal-errors', - 'backtrace' => array( array( 'file' => WP_PLUGIN_DIR . '3rd-plugin/file.php' ) ), + ); + } + + /** + * @testdox sanitize method correctly sanitizes paths + */ + public function test_sanitize() { + $message = WC_ABSPATH . 'includes/class-wc-test.php on line 123'; + $expected = '**/woocommerce/includes/class-wc-test.php on line 123'; + $result = $this->invoke_private_method( $this->sut, 'sanitize', array( $message ) ); + $this->assertEquals( $expected, $result ); + } + + /** + * @testdox sanitize_trace method correctly sanitizes stack traces + */ + public function test_sanitize_trace() { + $trace = array( + WC_ABSPATH . 'includes/class-wc-test.php:123', + ABSPATH . 'wp-includes/plugin.php:456', + ); + $expected = "**/woocommerce/includes/class-wc-test.php:123\n**/wp-includes/plugin.php:456"; + $result = $this->invoke_private_method( $this->sut, 'sanitize_trace', array( $trace ) ); + $this->assertEquals( $expected, $result ); + } + + /** + * Setup common conditions for remote logging tests. + * + * @param bool $enabled Whether remote logging is enabled. + */ + private function setup_remote_logging_conditions( $enabled = true ) { + update_option( 'woocommerce_feature_remote_logging_enabled', $enabled ? 'yes' : 'no' ); + add_filter( 'option_woocommerce_allow_tracking', fn() => 'yes' ); + add_filter( 'option_woocommerce_remote_variant_assignment', fn() => 5 ); + $this->setup_mock_plugin_updates( $enabled ? WC()->version : '9.0.0' ); + } + + /** + * Set up mock plugin updates. + * + * @param string $new_version The new version of WooCommerce to simulate. + */ + private function setup_mock_plugin_updates( $new_version ) { + $update_plugins = (object) array( + 'response' => array( + WC_PLUGIN_BASENAME => (object) array( + 'new_version' => $new_version, + 'package' => 'https://downloads.wordpress.org/plugin/woocommerce.zip', + 'slug' => 'woocommerce', + ), ), - true, - ), - ); - } + ); + add_filter( 'pre_site_transient_update_plugins', fn() => $update_plugins ); + } - /** - * @testdox sanitize method correctly sanitizes paths - */ - public function test_sanitize() { - $message = WC_ABSPATH . 'includes/class-wc-test.php on line 123'; - $expected = '**/woocommerce/includes/class-wc-test.php on line 123'; - $result = $this->invoke_private_method( $this->sut, 'sanitize', array( $message ) ); - $this->assertEquals( $expected, $result ); + /** + * Helper method to invoke private methods. + * + * @param object $obj Object instance. + * @param string $method_name Name of the private method. + * @param array $parameters Parameters to pass to the method. + * @return mixed + */ + private function invoke_private_method( $obj, $method_name, $parameters = array() ) { + $reflection = new \ReflectionClass( get_class( $obj ) ); + $method = $reflection->getMethod( $method_name ); + $method->setAccessible( true ); + return $method->invokeArgs( $obj, $parameters ); + } } - /** - * @testdox sanitize_trace method correctly sanitizes stack traces - */ - public function test_sanitize_trace() { - $trace = array( - WC_ABSPATH . 'includes/class-wc-test.php:123', - ABSPATH . 'wp-includes/plugin.php:456', - ); - $expected = "**/woocommerce/includes/class-wc-test.php:123\n**/wp-includes/plugin.php:456"; - $result = $this->invoke_private_method( $this->sut, 'sanitize_trace', array( $trace ) ); - $this->assertEquals( $expected, $result ); - } - - /** - * Setup common conditions for remote logging tests. - * - * @param bool $enabled Whether remote logging is enabled. - */ - private function setup_remote_logging_conditions( $enabled = true ) { - update_option( 'woocommerce_feature_remote_logging_enabled', $enabled ? 'yes' : 'no' ); - add_filter( 'option_woocommerce_allow_tracking', fn() => 'yes' ); - add_filter( 'option_woocommerce_remote_variant_assignment', fn() => 5 ); - $this->setup_mock_plugin_updates( $enabled ? WC()->version : '9.0.0' ); - } - - - /** - * Set up mock plugin updates. - * - * @param string $new_version The new version of WooCommerce to simulate. - */ - private function setup_mock_plugin_updates( $new_version ) { - $update_plugins = (object) array( - 'response' => array( - WC_PLUGIN_BASENAME => (object) array( - 'new_version' => $new_version, - 'package' => 'https://downloads.wordpress.org/plugin/woocommerce.zip', - 'slug' => 'woocommerce', - ), - ), - ); - add_filter( 'pre_site_transient_update_plugins', fn() => $update_plugins ); - } - - /** - * Helper method to invoke private methods. - * - * @param object $obj Object instance. - * @param string $method_name Name of the private method. - * @param array $parameters Parameters to pass to the method. - * @return mixed - */ - private function invoke_private_method( $obj, $method_name, $parameters = array() ) { - $reflection = new \ReflectionClass( get_class( $obj ) ); - $method = $reflection->getMethod( $method_name ); - $method->setAccessible( true ); - return $method->invokeArgs( $obj, $parameters ); - } -} - //phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound, Squiz.Classes.ClassFileName.NoMatch, Suin.Classes.PSR4.IncorrectClassName -/** - * Mock class that extends RemoteLogger to allow overriding is_dev_or_local_environment. - */ -class RemoteLoggerWithEnvironmentOverride extends RemoteLogger { /** - * The is_dev_or_local value. - * - * @var bool + * Mock class that extends RemoteLogger to allow overriding is_dev_or_local_environment. */ - private $is_dev_or_local = false; + class RemoteLoggerWithEnvironmentOverride extends RemoteLogger { + /** + * The is_dev_or_local value. + * + * @var bool + */ + private $is_dev_or_local = false; - /** - * Set the is_dev_or_local value. - * - * @param bool $value The value to set. - */ - public function set_is_dev_or_local( $value ) { - $this->is_dev_or_local = $value; + /** + * Set the is_dev_or_local value. + * + * @param bool $value The value to set. + */ + public function set_is_dev_or_local( $value ) { + $this->is_dev_or_local = $value; + } + + /** + * @inheritDoc + */ + protected function is_dev_or_local_environment() { + return $this->is_dev_or_local; + } } +//phpcs:enable Generic.Files.OneObjectStructurePerFile.MultipleFound, Squiz.Classes.ClassFileName.NoMatch, Suin.Classes.PSR4.IncorrectClassName +} +/** + * Mocks for global functions used in RemoteLogger.php + */ +namespace Automattic\WooCommerce\Internal\Logging { /** - * @inheritDoc + * The filter_input function will return NULL if we change the $_SERVER variables at runtime, so we + * need to override it in RemoteLogger's namespace when we want it to return a specific value for testing. + * + * @return mixed */ - protected function is_dev_or_local_environment() { - return $this->is_dev_or_local; + function filter_input() { + global $mock_filter_input, $mock_return; + + if ( true === $mock_filter_input ) { + return $mock_return; + } else { + return call_user_func_array( '\filter_input', func_get_args() ); + } } } -//phpcs:enable Generic.Files.OneObjectStructurePerFile.MultipleFound, Squiz.Classes.ClassFileName.NoMatch, Suin.Classes.PSR4.IncorrectClassName From 2433664aa8460590b636ff145600f1a3aa7b64f3 Mon Sep 17 00:00:00 2001 From: Manish Menaria Date: Mon, 2 Sep 2024 12:39:33 +0530 Subject: [PATCH 008/187] Product Collection - Show product picker in Editor when collection requires a product but it doesn't exist (#50164) * Show product picker control in the editor when a product context is required but not provided Enhanced the Product Collection block by introducing the `selectedReference` attribute and implementing a product picker control. This control appears in the editor when a product context is required but not provided in the current template/page/post. 1. **block.json**: Added `selectedReference` attribute of type `object`. 2. **constants.ts**: Included `selectedReference` in the `queryContextIncludes` array. 3. **EditorProductPicker.tsx**: Created a new component for selecting products within the editor. 4. **editor.scss**: Added styles for the new Editor Product Picker component. 5. **index.tsx**: Updated logic to determine the component to render, incorporating the new Editor Product Picker. 6. **types.ts**: Defined types for `selectedReference` and updated `ProductCollectionAttributes` interface. This enhancement allows merchants to manually select a product for collections that require a product context, ensuring the block displays correctly even when the product context is not available in the current template/page/post. - **Product Picker Control**: Utilizes the existing `ProductControl` component for selecting products. This component is displayed in the editor when a collection requires a product context but it doesn't exist in the current template/page/post. * Update label on ProductControl component * Implement dynamic UI state management for product collection block - Introduced `ProductCollectionUIStatesInEditor` enum to define various UI states for the product collection block. - Added `getProductCollectionUIStateInEditor` utility function to determine the appropriate UI state based on context. - Updated `Edit` component to use `getProductCollectionUIStateInEditor` for dynamic state management. - Refactored `ProductCollectionContent` to utilize the new Editor UI state management. * Fix: Product picker isn't showing * Fix: Preview label state isn't showing * Add changefile(s) from automation for the following project(s): woocommerce-blocks * Refactor WooCommerceBlockLocation type - Introduced specific interfaces for WooCommerceBlockLocation, including ProductLocation, ArchiveLocation, CartLocation, OrderLocation, and SiteLocation, to improve type safety and code clarity. - Updated createLocationObject function to return a BaseLocation type. - Refactored useSetPreviewState hook in product-collection utils: - Extracted termId from location.sourceData for cleaner and more readable code. - Replaced direct access of location.sourceData?.termId with termId variable. * Remove fallback to 0 in case there may be a product with id 0 * Use optional chaining to avoid undefined errors * Rename to * Change order of arguments in function * Pass boolean prop instead of making further recognition of the UI state in ProductCollectionContent * Destructure props in component * Rename to * Update names in enum * Rename to and change the structure to single number. * Rename location to * Add a method to choose a product in the product picker in Editor * Add E2E tests * Fix failing e2e tests by changing location to productCollectionLocation * Add changefile(s) from automation for the following project(s): woocommerce-blocks * Don't allow selecting product variations * Minor code refactoring * Fix: Product control isn't showing products **Before** ```tsx const getRenderItemFunc = () => { if ( renderItem ) { return renderItem; } else if ( showVariations ) { return renderItemWithVariations; } return () => null; }; ``` As you can see above, `return () => null;` is returning a function which is causing the issue. This will render nothing in the list. I changed this to `return undefined;`. This way, we will use default render item function. * Translate text in ProductPicker component * Improve E2E test * Use createInterpolateElement to safely render strong HTML tag * Fix E2E tests * Fix E2E tests * Product Collection: Inspector control to change selected product (#50590) * Add Linked Product Control to Product Collection Block Inspector Controls - Introduced a new `LinkedProductControl` component in the Product Collection block's Inspector Controls. - This control allows users to link a specific product to the product collection via a dropdown with a search capability. - Added corresponding styles to `editor.scss`. - Integrated a `useGetProduct` hook in the `utils.tsx` to fetch and manage the state of the linked product data, including handling loading states and errors. - Updated the Inspector Controls to include the new Linked Product Control component, enhancing the block's customization options for users. * Add E2E tests * Hide product picker when product context is available * Improve logic to hide Linked Product Control * Add changefile(s) from automation for the following project(s): woocommerce-blocks * Remove hasError state from useGetProduct hook * Rename isShowLinkedProductControl to showLinkedProductControl * Convert jsxProductButton to ProductButton component * Refactor jsxPopoverContent to LinkedProductPopoverContent component * Improve UI of Linked Product Control * Address PR feedback * Fix E2E tests --------- Co-authored-by: github-actions * Rename isUsesReferencePreviewMode to isUsingReferencePreviewMode * Change order of conditions in getProductCollectionUIStateInEditor --------- Co-authored-by: github-actions --- .../product-collection/edit/ProductPicker.tsx | 81 +++++ .../product-collection/edit/editor.scss | 46 +++ .../blocks/product-collection/edit/index.tsx | 46 ++- .../edit/inspector-controls/index.tsx | 8 + .../linked-product-control.tsx | 166 +++++++++ .../edit/product-collection-content.tsx | 13 +- .../js/blocks/product-collection/types.ts | 13 + .../js/blocks/product-collection/utils.tsx | 177 ++++++--- .../js/blocks/product-template/edit.tsx | 2 +- .../js/blocks/product-template/utils.tsx | 70 +++- .../product-control/index.tsx | 17 +- .../product-collection.block_theme.spec.ts | 335 +++++++++++++++++- .../product-collection.page.ts | 57 ++- ...-context-linking-a-product-with-collection | 4 + ...-product-with-collection-inspector-control | 4 + 15 files changed, 953 insertions(+), 86 deletions(-) create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/ProductPicker.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/linked-product-control.tsx create mode 100644 plugins/woocommerce/changelog/50164-add-44877-context-linking-a-product-with-collection create mode 100644 plugins/woocommerce/changelog/50590-add-44877-context-linking-a-product-with-collection-inspector-control diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/ProductPicker.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/ProductPicker.tsx new file mode 100644 index 00000000000..3d80edf035c --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/ProductPicker.tsx @@ -0,0 +1,81 @@ +/** + * External dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { useBlockProps } from '@wordpress/block-editor'; +import { Icon, info } from '@wordpress/icons'; +import ProductControl from '@woocommerce/editor-components/product-control'; +import type { SelectedOption } from '@woocommerce/block-hocs'; +import { createInterpolateElement } from '@wordpress/element'; +import { + Placeholder, + // @ts-expect-error Using experimental features + __experimentalHStack as HStack, + // @ts-expect-error Using experimental features + __experimentalText as Text, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import type { ProductCollectionEditComponentProps } from '../types'; +import { getCollectionByName } from '../collections'; + +const ProductPicker = ( props: ProductCollectionEditComponentProps ) => { + const blockProps = useBlockProps(); + const attributes = props.attributes; + + const collection = getCollectionByName( attributes.collection ); + if ( ! collection ) { + return; + } + + return ( +
+ + + + + { createInterpolateElement( + sprintf( + /* translators: %s: collection title */ + __( + '%s requires a product to be selected in order to display associated items.', + 'woocommerce' + ), + collection.title + ), + { + strong: , + } + ) } + + + { + const isValidId = ( value[ 0 ]?.id ?? null ) !== null; + if ( isValidId ) { + props.setAttributes( { + query: { + ...attributes.query, + productReference: value[ 0 ].id, + }, + } ); + } + } } + messages={ { + search: __( 'Select a product', 'woocommerce' ), + } } + /> + +
+ ); +}; + +export default ProductPicker; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/editor.scss index 63ecbc2f692..4dac0fe0dc5 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/editor.scss +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/editor.scss @@ -168,3 +168,49 @@ $max-button-width: calc(100% / #{$max-button-columns}); color: var(--wp-components-color-accent-inverted, #fff); } } + +// Editor Product Picker +.wc-blocks-product-collection__editor-product-picker { + .wc-blocks-product-collection__info-icon { + fill: var(--wp--preset--color--luminous-vivid-orange, #e26f56); + } +} + +// Linked Product Control +.wc-block-product-collection-linked-product-control { + width: 100%; + text-align: left; + + &__button { + width: 100%; + height: 100%; + padding: 10px; + border: 1px solid $gray-300; + } + + &__image-container { + flex-shrink: 0; + width: 45px; + height: 45px; + + img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + } + } + + &__content { + text-align: left; + } +} + +.wc-block-product-collection-linked-product__popover-content .components-popover__content { + width: 100%; + + .woocommerce-search-list__search { + border: 0; + padding: 0; + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/index.tsx index de9dcb3c03d..4206a024714 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/index.tsx @@ -4,18 +4,25 @@ import { store as blockEditorStore } from '@wordpress/block-editor'; import { useState } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; +import { useGetLocation } from '@woocommerce/blocks/product-template/utils'; /** * Internal dependencies */ -import type { ProductCollectionEditComponentProps } from '../types'; +import { + ProductCollectionEditComponentProps, + ProductCollectionUIStatesInEditor, +} from '../types'; import ProductCollectionPlaceholder from './product-collection-placeholder'; import ProductCollectionContent from './product-collection-content'; import CollectionSelectionModal from './collection-selection-modal'; import './editor.scss'; +import { getProductCollectionUIStateInEditor } from '../utils'; +import ProductPicker from './ProductPicker'; const Edit = ( props: ProductCollectionEditComponentProps ) => { const { clientId, attributes } = props; + const location = useGetLocation( props.context, props.clientId ); const [ isSelectionModalOpen, setIsSelectionModalOpen ] = useState( false ); const hasInnerBlocks = useSelect( @@ -24,9 +31,37 @@ const Edit = ( props: ProductCollectionEditComponentProps ) => { [ clientId ] ); - const Component = hasInnerBlocks - ? ProductCollectionContent - : ProductCollectionPlaceholder; + const productCollectionUIStateInEditor = + getProductCollectionUIStateInEditor( { + hasInnerBlocks, + location, + attributes: props.attributes, + usesReference: props.usesReference, + } ); + + /** + * Component to render based on the UI state. + */ + let Component, + isUsingReferencePreviewMode = false; + switch ( productCollectionUIStateInEditor ) { + case ProductCollectionUIStatesInEditor.COLLECTION_PICKER: + Component = ProductCollectionPlaceholder; + break; + case ProductCollectionUIStatesInEditor.PRODUCT_REFERENCE_PICKER: + Component = ProductPicker; + break; + case ProductCollectionUIStatesInEditor.VALID: + Component = ProductCollectionContent; + break; + case ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW: + Component = ProductCollectionContent; + isUsingReferencePreviewMode = true; + break; + default: + // By default showing collection chooser. + Component = ProductCollectionPlaceholder; + } return ( <> @@ -35,6 +70,9 @@ const Edit = ( props: ProductCollectionEditComponentProps ) => { openCollectionSelectionModal={ () => setIsSelectionModalOpen( true ) } + isUsingReferencePreviewMode={ isUsingReferencePreviewMode } + location={ location } + usesReference={ props.usesReference } /> { isSelectionModalOpen && ( ( filter: FilterName ) => { @@ -121,6 +122,13 @@ const ProductCollectionInspectorControls = ( return ( + + { diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/linked-product-control.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/linked-product-control.tsx new file mode 100644 index 00000000000..35606aff0a9 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/linked-product-control.tsx @@ -0,0 +1,166 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import ProductControl from '@woocommerce/editor-components/product-control'; +import { SelectedOption } from '@woocommerce/block-hocs'; +import { useState, useMemo } from '@wordpress/element'; +import type { WooCommerceBlockLocation } from '@woocommerce/blocks/product-template/utils'; +import type { ProductResponseItem } from '@woocommerce/types'; +import { decodeEntities } from '@wordpress/html-entities'; +import { + PanelBody, + PanelRow, + Button, + Flex, + FlexItem, + Dropdown, + // @ts-expect-error Using experimental features + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalText as Text, + Spinner, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { useGetProduct } from '../../utils'; +import type { + ProductCollectionQuery, + ProductCollectionSetAttributes, +} from '../../types'; + +const ProductButton: React.FC< { + isOpen: boolean; + onToggle: () => void; + product: ProductResponseItem | null; + isLoading: boolean; +} > = ( { isOpen, onToggle, product, isLoading } ) => { + if ( isLoading && ! product ) { + return ; + } + + return ( + + ); +}; + +const LinkedProductPopoverContent: React.FC< { + query: ProductCollectionQuery; + setAttributes: ProductCollectionSetAttributes; + setIsDropdownOpen: React.Dispatch< React.SetStateAction< boolean > >; +} > = ( { query, setAttributes, setIsDropdownOpen } ) => ( + { + const productId = value[ 0 ]?.id ?? null; + if ( productId !== null ) { + setAttributes( { + query: { + ...query, + productReference: productId, + }, + } ); + setIsDropdownOpen( false ); + } + } } + messages={ { + search: __( 'Select a product', 'woocommerce' ), + } } + /> +); + +const LinkedProductControl = ( { + query, + setAttributes, + location, + usesReference, +}: { + query: ProductCollectionQuery; + setAttributes: ProductCollectionSetAttributes; + location: WooCommerceBlockLocation; + usesReference: string[] | undefined; +} ) => { + const [ isDropdownOpen, setIsDropdownOpen ] = useState< boolean >( false ); + const { product, isLoading } = useGetProduct( query.productReference ); + + const showLinkedProductControl = useMemo( () => { + const isInRequiredLocation = usesReference?.includes( location.type ); + const isProductContextRequired = usesReference?.includes( 'product' ); + const isProductContextSelected = + ( query?.productReference ?? null ) !== null; + + return ( + isProductContextRequired && + ! isInRequiredLocation && + isProductContextSelected + ); + }, [ location.type, query?.productReference, usesReference ] ); + + if ( ! showLinkedProductControl ) return null; + + return ( + + + ( + + ) } + renderContent={ () => ( + + ) } + open={ isDropdownOpen } + onToggle={ () => setIsDropdownOpen( ! isDropdownOpen ) } + /> + + + ); +}; + +export default LinkedProductControl; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/product-collection-content.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/product-collection-content.tsx index dadbddb7751..35714946c42 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/product-collection-content.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/product-collection-content.tsx @@ -10,7 +10,6 @@ import { useInstanceId } from '@wordpress/compose'; import { useEffect, useRef, useMemo } from '@wordpress/element'; import { Button } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; -import { useGetLocation } from '@woocommerce/blocks/product-template/utils'; import fastDeepEqual from 'fast-deep-equal/es6'; /** @@ -68,19 +67,23 @@ const useQueryId = ( const ProductCollectionContent = ( { preview: { setPreviewState, initialPreviewState } = {}, - usesReference, ...props }: ProductCollectionEditComponentProps ) => { const isInitialAttributesSet = useRef( false ); - const { clientId, attributes, setAttributes } = props; - const location = useGetLocation( props.context, props.clientId ); + const { + clientId, + attributes, + setAttributes, + location, + isUsingReferencePreviewMode, + } = props; useSetPreviewState( { setPreviewState, setAttributes, location, attributes, - usesReference, + isUsingReferencePreviewMode, } ); const blockProps = useBlockProps(); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts index 4407c682abe..55a8ee9b460 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts @@ -9,6 +9,16 @@ import { type AttributeMetadata } from '@woocommerce/types'; */ import { WooCommerceBlockLocation } from '../product-template/utils'; +export enum ProductCollectionUIStatesInEditor { + COLLECTION_PICKER = 'collection_chooser', + PRODUCT_REFERENCE_PICKER = 'product_context_picker', + VALID_WITH_PREVIEW = 'uses_reference_preview_mode', + VALID = 'valid', + // Future states + // INVALID = 'invalid', + // DELETED_PRODUCT_REFERENCE = 'deleted_product_reference', +} + export interface ProductCollectionAttributes { query: ProductCollectionQuery; queryId: number; @@ -95,6 +105,7 @@ export interface ProductCollectionQuery { woocommerceHandPickedProducts: string[]; priceRange: undefined | PriceRange; filterable: boolean; + productReference?: number; } export type ProductCollectionEditComponentProps = @@ -108,6 +119,8 @@ export type ProductCollectionEditComponentProps = context: { templateSlug: string; }; + isUsingReferencePreviewMode: boolean; + location: WooCommerceBlockLocation; }; export type TProductCollectionOrder = 'asc' | 'desc'; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/utils.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/utils.tsx index bdbd882e88a..0565027bfe1 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/utils.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/utils.tsx @@ -6,8 +6,10 @@ import { addFilter } from '@wordpress/hooks'; import { select } from '@wordpress/data'; import { isWpVersion } from '@woocommerce/settings'; import type { BlockEditProps, Block } from '@wordpress/blocks'; -import { useLayoutEffect } from '@wordpress/element'; +import { useEffect, useLayoutEffect, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import type { ProductResponseItem } from '@woocommerce/types'; +import { getProduct } from '@woocommerce/editor-components/utils'; import { createBlock, // @ts-expect-error Type definitions for this function are missing in Guteberg @@ -18,13 +20,14 @@ import { * Internal dependencies */ import { - type ProductCollectionAttributes, - type TProductCollectionOrder, - type TProductCollectionOrderBy, - type ProductCollectionQuery, - type ProductCollectionDisplayLayout, - type PreviewState, - type SetPreviewState, + ProductCollectionAttributes, + TProductCollectionOrder, + TProductCollectionOrderBy, + ProductCollectionQuery, + ProductCollectionDisplayLayout, + PreviewState, + SetPreviewState, + ProductCollectionUIStatesInEditor, } from './types'; import { coreQueryPaginationBlockName, @@ -166,41 +169,14 @@ export const addProductCollectionToQueryPaginationParentOrAncestor = () => { }; /** - * Get the preview message for the Product Collection block based on the usesReference. - * There are two scenarios: - * 1. When usesReference is product, the preview message will be: - * "Actual products will vary depending on the product being viewed." - * 2. For all other usesReference, the preview message will be: - * "Actual products will vary depending on the page being viewed." - * - * This message will be shown when the usesReference isn't available on the Editor side, but is available on the Frontend. + * Get the message to show in the preview label when the block is in preview mode based + * on the `usesReference` value. */ export const getUsesReferencePreviewMessage = ( location: WooCommerceBlockLocation, - usesReference?: string[] + isUsingReferencePreviewMode: boolean ) => { - if ( ! ( Array.isArray( usesReference ) && usesReference.length > 0 ) ) { - return ''; - } - - if ( usesReference.includes( location.type ) ) { - /** - * Block shouldn't be in preview mode when: - * 1. Current location is archive and termId is available. - * 2. Current location is product and productId is available. - * - * Because in these cases, we have required context on the editor side. - */ - const isArchiveLocationWithTermId = - location.type === LocationType.Archive && - ( location.sourceData?.termId ?? null ) !== null; - const isProductLocationWithProductId = - location.type === LocationType.Product && - ( location.sourceData?.productId ?? null ) !== null; - if ( isArchiveLocationWithTermId || isProductLocationWithProductId ) { - return ''; - } - + if ( isUsingReferencePreviewMode ) { if ( location.type === LocationType.Product ) { return __( 'Actual products will vary depending on the product being viewed.', @@ -217,12 +193,77 @@ export const getUsesReferencePreviewMessage = ( return ''; }; +export const getProductCollectionUIStateInEditor = ( { + location, + usesReference, + attributes, + hasInnerBlocks, +}: { + location: WooCommerceBlockLocation; + usesReference?: string[] | undefined; + attributes: ProductCollectionAttributes; + hasInnerBlocks: boolean; +} ): ProductCollectionUIStatesInEditor => { + const isInRequiredLocation = usesReference?.includes( location.type ); + const isCollectionSelected = !! attributes.collection; + + /** + * Case 1: Product context picker + */ + const isProductContextRequired = usesReference?.includes( 'product' ); + const isProductContextSelected = + ( attributes.query?.productReference ?? null ) !== null; + if ( + isCollectionSelected && + isProductContextRequired && + ! isInRequiredLocation && + ! isProductContextSelected + ) { + return ProductCollectionUIStatesInEditor.PRODUCT_REFERENCE_PICKER; + } + + /** + * Case 2: Preview mode - based on `usesReference` value + */ + if ( isInRequiredLocation ) { + /** + * Block shouldn't be in preview mode when: + * 1. Current location is archive and termId is available. + * 2. Current location is product and productId is available. + * + * Because in these cases, we have required context on the editor side. + */ + const isArchiveLocationWithTermId = + location.type === LocationType.Archive && + ( location.sourceData?.termId ?? null ) !== null; + const isProductLocationWithProductId = + location.type === LocationType.Product && + ( location.sourceData?.productId ?? null ) !== null; + + if ( + ! isArchiveLocationWithTermId && + ! isProductLocationWithProductId + ) { + return ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW; + } + } + + /** + * Case 3: Collection chooser + */ + if ( ! hasInnerBlocks && ! isCollectionSelected ) { + return ProductCollectionUIStatesInEditor.COLLECTION_PICKER; + } + + return ProductCollectionUIStatesInEditor.VALID; +}; + export const useSetPreviewState = ( { setPreviewState, location, attributes, setAttributes, - usesReference, + isUsingReferencePreviewMode, }: { setPreviewState?: SetPreviewState | undefined; location: WooCommerceBlockLocation; @@ -231,6 +272,7 @@ export const useSetPreviewState = ( { attributes: Partial< ProductCollectionAttributes > ) => void; usesReference?: string[] | undefined; + isUsingReferencePreviewMode: boolean; } ) => { const setState = ( newPreviewState: PreviewState ) => { setAttributes( { @@ -240,8 +282,6 @@ export const useSetPreviewState = ( { }, } ); }; - const isCollectionUsesReference = - usesReference && usesReference?.length > 0; /** * When usesReference is available on Frontend but not on Editor side, @@ -249,10 +289,10 @@ export const useSetPreviewState = ( { */ const usesReferencePreviewMessage = getUsesReferencePreviewMessage( location, - usesReference + isUsingReferencePreviewMode ); useLayoutEffect( () => { - if ( isCollectionUsesReference ) { + if ( isUsingReferencePreviewMode ) { setAttributes( { __privatePreviewState: { isPreview: usesReferencePreviewMessage.length > 0, @@ -263,12 +303,12 @@ export const useSetPreviewState = ( { }, [ setAttributes, usesReferencePreviewMessage, - isCollectionUsesReference, + isUsingReferencePreviewMode, ] ); // Running setPreviewState function provided by Collection, if it exists. useLayoutEffect( () => { - if ( ! setPreviewState && ! isCollectionUsesReference ) { + if ( ! setPreviewState && ! isUsingReferencePreviewMode ) { return; } @@ -294,11 +334,14 @@ export const useSetPreviewState = ( { * - Products by tag * - Products by attribute */ + const termId = + location.type === LocationType.Archive + ? location.sourceData?.termId + : null; useLayoutEffect( () => { - if ( ! setPreviewState && ! isCollectionUsesReference ) { + if ( ! setPreviewState && ! isUsingReferencePreviewMode ) { const isGenericArchiveTemplate = - location.type === LocationType.Archive && - location.sourceData?.termId === null; + location.type === LocationType.Archive && termId === null; setAttributes( { __privatePreviewState: { @@ -315,11 +358,11 @@ export const useSetPreviewState = ( { }, [ attributes?.query?.inherit, usesReferencePreviewMessage, - location.sourceData?.termId, + termId, location.type, setAttributes, setPreviewState, - isCollectionUsesReference, + isUsingReferencePreviewMode, ] ); }; @@ -356,3 +399,35 @@ export const getDefaultProductCollection = () => }, createBlocksFromInnerBlocksTemplate( INNER_BLOCKS_TEMPLATE ) ); + +export const useGetProduct = ( productId: number | undefined ) => { + const [ product, setProduct ] = useState< ProductResponseItem | null >( + null + ); + const [ isLoading, setIsLoading ] = useState< boolean >( false ); + + useEffect( () => { + const fetchProduct = async () => { + if ( productId ) { + setIsLoading( true ); + try { + const fetchedProduct = ( await getProduct( + productId + ) ) as ProductResponseItem; + setProduct( fetchedProduct ); + } catch ( error ) { + setProduct( null ); + } finally { + setIsLoading( false ); + } + } else { + setProduct( null ); + setIsLoading( false ); + } + }; + + fetchProduct(); + }, [ productId ] ); + + return { product, isLoading }; +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-template/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-template/edit.tsx index 5c469725163..1c09035f46e 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-template/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-template/edit.tsx @@ -266,7 +266,7 @@ const ProductTemplateEdit = ( products: getEntityRecords( 'postType', postType, { ...query, ...restQueryArgs, - location, + productCollectionLocation: location, productCollectionQueryContext, previewState: __privateProductCollectionPreviewState, /** diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-template/utils.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-template/utils.tsx index 5f5344a7296..9106d24ba3c 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-template/utils.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-template/utils.tsx @@ -63,17 +63,65 @@ const prepareIsInGenericTemplate = ( entitySlug: string ): boolean => templateSlug === entitySlug; -export type WooCommerceBlockLocation = ReturnType< - typeof createLocationObject ->; +interface WooCommerceBaseLocation { + type: LocationType; + sourceData?: object | undefined; +} -const createLocationObject = ( - type: LocationType, - sourceData: Record< string, unknown > = {} -) => ( { - type, - sourceData, -} ); +interface ProductLocation extends WooCommerceBaseLocation { + type: LocationType.Product; + sourceData?: + | { + productId: number; + } + | undefined; +} + +interface ArchiveLocation extends WooCommerceBaseLocation { + type: LocationType.Archive; + sourceData?: + | { + taxonomy: string; + termId: number; + } + | undefined; +} + +interface CartLocation extends WooCommerceBaseLocation { + type: LocationType.Cart; + sourceData?: + | { + productIds: number[]; + } + | undefined; +} + +interface OrderLocation extends WooCommerceBaseLocation { + type: LocationType.Order; + sourceData?: + | { + orderId: number; + } + | undefined; +} + +interface SiteLocation extends WooCommerceBaseLocation { + type: LocationType.Site; + sourceData?: object | undefined; +} + +export type WooCommerceBlockLocation = + | ProductLocation + | ArchiveLocation + | CartLocation + | OrderLocation + | SiteLocation; + +const createLocationObject = ( type: LocationType, sourceData: object = {} ) => + ( { + type, + sourceData, + } as WooCommerceBlockLocation ); type ContextProperties = { templateSlug: string; @@ -83,7 +131,7 @@ type ContextProperties = { export const useGetLocation = < T, >( context: Context< T & ContextProperties >, clientId: string -) => { +): WooCommerceBlockLocation => { const templateSlug = context.templateSlug || ''; const postId = context.postId || null; diff --git a/plugins/woocommerce-blocks/assets/js/editor-components/product-control/index.tsx b/plugins/woocommerce-blocks/assets/js/editor-components/product-control/index.tsx index bfa1e5f2a85..f86f2878dc7 100644 --- a/plugins/woocommerce-blocks/assets/js/editor-components/product-control/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/editor-components/product-control/index.tsx @@ -62,6 +62,16 @@ interface ProductControlProps { * Whether to show variations in the list of items available. */ showVariations?: boolean; + /** + * Different messages to display in the component. + * If any of the messages are not provided, the default message will be used. + */ + messages?: { + list?: string; + noItems?: string; + search?: string; + updated?: string; + }; } const messages = { @@ -188,7 +198,7 @@ const ProductControl = ( } else if ( showVariations ) { return renderItemWithVariations; } - return () => null; + return undefined; }; if ( error ) { @@ -216,7 +226,10 @@ const ProductControl = ( onChange={ onChange } renderItem={ getRenderItemFunc() } onSearch={ onSearch } - messages={ messages } + messages={ { + ...messages, + ...props.messages, + } } isHierarchical /> ); 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 af0839c5ca1..e38db1b526a 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 @@ -9,6 +9,7 @@ import { test as base, expect } from '@woocommerce/e2e-utils'; */ import ProductCollectionPage, { BLOCK_LABELS, + Collections, SELECTORS, } from './product-collection.page'; @@ -402,7 +403,7 @@ test.describe( 'Product Collection', () => { } ); } ); - test.describe( 'Location is recognised', () => { + test.describe( 'Location is recognized', () => { const filterRequest = ( request: Request ) => { const url = request.url(); return ( @@ -418,7 +419,9 @@ test.describe( 'Product Collection', () => { return ( url.includes( 'wp/v2/product' ) && searchParams.get( 'isProductCollectionBlock' ) === 'true' && - !! searchParams.get( `location[sourceData][productId]` ) + !! searchParams.get( + `productCollectionLocation[sourceData][productId]` + ) ); }; @@ -430,26 +433,30 @@ test.describe( 'Product Collection', () => { if ( locationType === 'product' ) { return { - type: searchParams.get( 'location[type]' ), + type: searchParams.get( 'productCollectionLocation[type]' ), productId: searchParams.get( - `location[sourceData][productId]` + `productCollectionLocation[sourceData][productId]` ), }; } if ( locationType === 'archive' ) { return { - type: searchParams.get( 'location[type]' ), + type: searchParams.get( 'productCollectionLocation[type]' ), taxonomy: searchParams.get( - `location[sourceData][taxonomy]` + `productCollectionLocation[sourceData][taxonomy]` + ), + termId: searchParams.get( + `productCollectionLocation[sourceData][termId]` ), - termId: searchParams.get( `location[sourceData][termId]` ), }; } return { - type: searchParams.get( 'location[type]' ), - sourceData: searchParams.get( `location[sourceData]` ), + type: searchParams.get( 'productCollectionLocation[type]' ), + sourceData: searchParams.get( + `productCollectionLocation[sourceData]` + ), }; }; @@ -482,10 +489,10 @@ test.describe( 'Product Collection', () => { pageObject.BLOCK_NAME ); - const locationReuqestPromise = + const locationRequestPromise = page.waitForRequest( filterProductRequest ); await pageObject.chooseCollectionInTemplate( 'featured' ); - const locationRequest = await locationReuqestPromise; + const locationRequest = await locationRequestPromise; const { type, productId } = getLocationDetailsFromRequest( locationRequest, @@ -961,3 +968,309 @@ test.describe( 'Product Collection', () => { } ); } ); } ); + +test.describe( 'Testing "usesReference" argument in "registerProductCollection"', () => { + const MY_REGISTERED_COLLECTIONS = { + myCustomCollectionWithProductContext: { + name: 'My Custom Collection - Product Context', + label: 'Block: My Custom Collection - Product Context', + previewLabelTemplate: [ 'woocommerce/woocommerce//single-product' ], + shouldShowProductPicker: true, + }, + myCustomCollectionWithCartContext: { + name: 'My Custom Collection - Cart Context', + label: 'Block: My Custom Collection - Cart Context', + previewLabelTemplate: [ 'woocommerce/woocommerce//page-cart' ], + shouldShowProductPicker: false, + }, + myCustomCollectionWithOrderContext: { + name: 'My Custom Collection - Order Context', + label: 'Block: My Custom Collection - Order Context', + previewLabelTemplate: [ + 'woocommerce/woocommerce//order-confirmation', + ], + shouldShowProductPicker: false, + }, + myCustomCollectionWithArchiveContext: { + name: 'My Custom Collection - Archive Context', + label: 'Block: My Custom Collection - Archive Context', + previewLabelTemplate: [ + 'woocommerce/woocommerce//taxonomy-product_cat', + ], + shouldShowProductPicker: false, + }, + myCustomCollectionMultipleContexts: { + name: 'My Custom Collection - Multiple Contexts', + label: 'Block: My Custom Collection - Multiple Contexts', + previewLabelTemplate: [ + 'woocommerce/woocommerce//single-product', + 'woocommerce/woocommerce//order-confirmation', + ], + shouldShowProductPicker: true, + }, + }; + + // Activate plugin which registers custom product collections + test.beforeEach( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( + 'register-product-collection-tester' + ); + } ); + + Object.entries( MY_REGISTERED_COLLECTIONS ).forEach( + ( [ key, collection ] ) => { + for ( const template of collection.previewLabelTemplate ) { + test( `Collection "${ collection.name }" should show preview label in "${ template }"`, async ( { + pageObject, + editor, + } ) => { + await pageObject.goToEditorTemplate( template ); + await pageObject.insertProductCollection(); + await pageObject.chooseCollectionInTemplate( + key as Collections + ); + + const block = editor.canvas.getByLabel( collection.label ); + const previewButtonLocator = block.getByTestId( + SELECTORS.previewButtonTestID + ); + + await expect( previewButtonLocator ).toBeVisible(); + } ); + } + + test( `Collection "${ collection.name }" should not show preview label in a post`, async ( { + pageObject, + editor, + admin, + } ) => { + await admin.createNewPost(); + await pageObject.insertProductCollection(); + await pageObject.chooseCollectionInPost( key as Collections ); + + // Check visibility of product picker + const editorProductPicker = editor.canvas.locator( + SELECTORS.productPicker + ); + const expectedVisibility = collection.shouldShowProductPicker + ? 'toBeVisible' + : 'toBeHidden'; + await expect( editorProductPicker )[ expectedVisibility ](); + + if ( collection.shouldShowProductPicker ) { + await pageObject.chooseProductInEditorProductPickerIfAvailable( + editor.canvas + ); + } + + // At this point, the product picker should be hidden + await expect( editorProductPicker ).toBeHidden(); + + // Check visibility of preview label + const block = editor.canvas.getByLabel( collection.label ); + const previewButtonLocator = block.getByTestId( + SELECTORS.previewButtonTestID + ); + + await expect( previewButtonLocator ).toBeHidden(); + } ); + + test( `Collection "${ collection.name }" should not show preview label in Product Catalog template`, async ( { + pageObject, + editor, + } ) => { + await pageObject.goToProductCatalogAndInsertCollection( + key as Collections + ); + + const block = editor.canvas.getByLabel( collection.label ); + const previewButtonLocator = block.getByTestId( + SELECTORS.previewButtonTestID + ); + + await expect( previewButtonLocator ).toBeHidden(); + } ); + } + ); +} ); + +test.describe( 'Product picker', () => { + const MY_REGISTERED_COLLECTIONS_THAT_NEEDS_PRODUCT = { + myCustomCollectionWithProductContext: { + name: 'My Custom Collection - Product Context', + label: 'Block: My Custom Collection - Product Context', + collection: + 'woocommerce/product-collection/my-custom-collection-product-context', + }, + myCustomCollectionMultipleContexts: { + name: 'My Custom Collection - Multiple Contexts', + label: 'Block: My Custom Collection - Multiple Contexts', + collection: + 'woocommerce/product-collection/my-custom-collection-multiple-contexts', + }, + }; + + // Activate plugin which registers custom product collections + test.beforeEach( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( + 'register-product-collection-tester' + ); + } ); + + Object.entries( MY_REGISTERED_COLLECTIONS_THAT_NEEDS_PRODUCT ).forEach( + ( [ key, collection ] ) => { + test( `For collection "${ collection.name }" - manually selected product reference should be available on Frontend in a post`, async ( { + pageObject, + admin, + page, + editor, + } ) => { + await admin.createNewPost(); + await pageObject.insertProductCollection(); + await pageObject.chooseCollectionInPost( key as Collections ); + + // Verify that product picker is shown in Editor + const editorProductPicker = editor.canvas.locator( + SELECTORS.productPicker + ); + await expect( editorProductPicker ).toBeVisible(); + + // Once a product is selected, the product picker should be hidden + await pageObject.chooseProductInEditorProductPickerIfAvailable( + editor.canvas + ); + await expect( editorProductPicker ).toBeHidden(); + + // On Frontend, verify that product reference is a number + await pageObject.publishAndGoToFrontend(); + const collectionWithProductContext = page.locator( + `[data-collection="${ collection.collection }"]` + ); + const queryAttribute = JSON.parse( + ( await collectionWithProductContext.getAttribute( + 'data-query' + ) ) || '{}' + ); + expect( typeof queryAttribute?.productReference ).toBe( + 'number' + ); + } ); + + test( `For collection "${ collection.name }" - changing product using inspector control`, async ( { + pageObject, + admin, + page, + editor, + } ) => { + await admin.createNewPost(); + await pageObject.insertProductCollection(); + await pageObject.chooseCollectionInPost( key as Collections ); + + // Verify that product picker is shown in Editor + const editorProductPicker = editor.canvas.locator( + SELECTORS.productPicker + ); + await expect( editorProductPicker ).toBeVisible(); + + // Once a product is selected, the product picker should be hidden + await pageObject.chooseProductInEditorProductPickerIfAvailable( + editor.canvas + ); + await expect( editorProductPicker ).toBeHidden(); + + // Verify that Album is selected + await expect( + admin.page.locator( SELECTORS.linkedProductControl.button ) + ).toContainText( 'Album' ); + + // Change product using inspector control to Beanie + await admin.page + .locator( SELECTORS.linkedProductControl.button ) + .click(); + await admin.page + .locator( SELECTORS.linkedProductControl.popoverContent ) + .getByLabel( 'Beanie', { exact: true } ) + .click(); + await expect( + admin.page.locator( SELECTORS.linkedProductControl.button ) + ).toContainText( 'Beanie' ); + + // On Frontend, verify that product reference is a number + await pageObject.publishAndGoToFrontend(); + const collectionWithProductContext = page.locator( + `[data-collection="${ collection.collection }"]` + ); + const queryAttribute = JSON.parse( + ( await collectionWithProductContext.getAttribute( + 'data-query' + ) ) || '{}' + ); + expect( typeof queryAttribute?.productReference ).toBe( + 'number' + ); + } ); + + test( `For collection "${ collection.name }" - product picker shouldn't be shown in Single Product template`, async ( { + pageObject, + admin, + editor, + } ) => { + await admin.visitSiteEditor( { + postId: `woocommerce/woocommerce//single-product`, + postType: 'wp_template', + canvas: 'edit', + } ); + await editor.canvas.locator( 'body' ).click(); + await pageObject.insertProductCollection(); + await pageObject.chooseCollectionInTemplate( + key as Collections + ); + + const editorProductPicker = editor.canvas.locator( + SELECTORS.productPicker + ); + await expect( editorProductPicker ).toBeHidden(); + } ); + } + ); + + test( 'Product picker should work as expected while changing collection using "Choose collection" button from Toolbar', async ( { + pageObject, + admin, + editor, + } ) => { + await admin.createNewPost(); + await pageObject.insertProductCollection(); + await pageObject.chooseCollectionInPost( + 'myCustomCollectionWithProductContext' + ); + + // Verify that product picker is shown in Editor + const editorProductPicker = editor.canvas.locator( + SELECTORS.productPicker + ); + await expect( editorProductPicker ).toBeVisible(); + + // Once a product is selected, the product picker should be hidden + await pageObject.chooseProductInEditorProductPickerIfAvailable( + editor.canvas + ); + await expect( editorProductPicker ).toBeHidden(); + + // Change collection using Toolbar + await pageObject.changeCollectionUsingToolbar( + 'myCustomCollectionMultipleContexts' + ); + await expect( editorProductPicker ).toBeVisible(); + + // Once a product is selected, the product picker should be hidden + await pageObject.chooseProductInEditorProductPickerIfAvailable( + editor.canvas + ); + await expect( editorProductPicker ).toBeHidden(); + + // Product picker should be hidden for collections that don't need product + await pageObject.changeCollectionUsingToolbar( 'featured' ); + await expect( editorProductPicker ).toBeHidden(); + } ); +} ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.page.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.page.ts index 22a0fb3e3e1..1a87ebeb605 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.page.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.page.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import { Locator, Page } from '@playwright/test'; +import { FrameLocator, Locator, Page } from '@playwright/test'; import { Editor, Admin } from '@woocommerce/e2e-utils'; import { BlockRepresentation } from '@wordpress/e2e-test-utils-playwright/build-types/editor/insert-block'; @@ -62,6 +62,12 @@ export const SELECTORS = { previewButtonTestID: 'product-collection-preview-button', collectionPlaceholder: '[data-type="woocommerce/product-collection"] .components-placeholder', + productPicker: '.wc-blocks-product-collection__editor-product-picker', + linkedProductControl: { + button: '.wc-block-product-collection-linked-product-control__button', + popoverContent: + '.wc-block-product-collection-linked-product__popover-content', + }, }; export type Collections = @@ -200,10 +206,31 @@ class ProductCollectionPage { } } + async chooseProductInEditorProductPickerIfAvailable( + pageReference: Page | FrameLocator + ) { + const editorProductPicker = pageReference.locator( + SELECTORS.productPicker + ); + + if ( await editorProductPicker.isVisible() ) { + await editorProductPicker + .locator( 'label' ) + .filter( { + hasText: 'Album', + } ) + .click(); + } + } + async createNewPostAndInsertBlock( collection?: Collections ) { await this.admin.createNewPost(); await this.insertProductCollection(); await this.chooseCollectionInPost( collection ); + // If product picker is available, choose a product. + await this.chooseProductInEditorProductPickerIfAvailable( + this.admin.page + ); await this.refreshLocators( 'editor' ); await this.editor.openDocumentSettingsSidebar(); } @@ -345,6 +372,10 @@ class ProductCollectionPage { await this.editor.canvas.locator( 'body' ).click(); await this.insertProductCollection(); await this.chooseCollectionInTemplate( collection ); + // If product picker is available, choose a product. + await this.chooseProductInEditorProductPickerIfAvailable( + this.editor.canvas + ); await this.refreshLocators( 'editor' ); } @@ -571,6 +602,30 @@ class ProductCollectionPage { .click(); } + async changeCollectionUsingToolbar( collection: Collections ) { + // Click "Choose collection" button in the toolbar. + await this.admin.page + .getByRole( 'toolbar', { name: 'Block Tools' } ) + .getByRole( 'button', { name: 'Choose collection' } ) + .click(); + + // Select the collection from the modal. + const collectionChooserModal = this.admin.page.locator( + '.wc-blocks-product-collection__modal' + ); + await collectionChooserModal + .getByRole( 'button', { + name: collectionToButtonNameMap[ collection ], + } ) + .click(); + + await collectionChooserModal + .getByRole( 'button', { + name: 'Continue', + } ) + .click(); + } + async setDisplaySettings( { itemsPerPage, offset, diff --git a/plugins/woocommerce/changelog/50164-add-44877-context-linking-a-product-with-collection b/plugins/woocommerce/changelog/50164-add-44877-context-linking-a-product-with-collection new file mode 100644 index 00000000000..1308c1e61e7 --- /dev/null +++ b/plugins/woocommerce/changelog/50164-add-44877-context-linking-a-product-with-collection @@ -0,0 +1,4 @@ +Significance: major +Type: add + +Product Collection - Show product picker in Editor when collection requires a product but not available
A collection can define if it requires a product context. This can be done using `usesReference` argument i.e. ```tsx __experimentalRegisterProductCollection({ ..., usesReference: ['product'], ) ``` When product context doesn't exist in current template/page/post etc. then we show product picker in Editor. This way, merchant can manually provide a product context to the collection. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/50590-add-44877-context-linking-a-product-with-collection-inspector-control b/plugins/woocommerce/changelog/50590-add-44877-context-linking-a-product-with-collection-inspector-control new file mode 100644 index 00000000000..2db092dad41 --- /dev/null +++ b/plugins/woocommerce/changelog/50590-add-44877-context-linking-a-product-with-collection-inspector-control @@ -0,0 +1,4 @@ +Significance: major +Type: add + +Product Collection - Implement Inspector control to change selected product \ No newline at end of file From 0f7773dd47df90e4c7c4e60c141b307e139202cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Mon, 2 Sep 2024 09:40:47 +0200 Subject: [PATCH 009/187] Fix My Account block icon being too small when inserted via block hooks (#51047) * Fix My Account block icon being too small when inserted via block hooks * Add changelog file --- .../woocommerce/changelog/fix-50667-my-account-hooked-size | 4 ++++ plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php | 1 + 2 files changed, 5 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-50667-my-account-hooked-size diff --git a/plugins/woocommerce/changelog/fix-50667-my-account-hooked-size b/plugins/woocommerce/changelog/fix-50667-my-account-hooked-size new file mode 100644 index 00000000000..4fcd396ddc4 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-50667-my-account-hooked-size @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix My Account block icon being too small when inserted via block hooks diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php b/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php index 80bd799b224..8d201c1051f 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php @@ -68,6 +68,7 @@ class CustomerAccount extends AbstractBlock { public function modify_hooked_block_attributes( $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ) { $parsed_hooked_block['attrs']['displayStyle'] = 'icon_only'; $parsed_hooked_block['attrs']['iconStyle'] = 'line'; + $parsed_hooked_block['attrs']['iconClass'] = 'wc-block-customer-account__account-icon'; /* * The Mini Cart block (which is hooked into the header) has a margin of 0.5em on the left side. From 74e96d689b4cd818c4dc6f764dba9a5a049400e5 Mon Sep 17 00:00:00 2001 From: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:09:56 +0100 Subject: [PATCH 010/187] [e2e tests] Tag some e2e tests to help with test audit (#51044) --- .../woocommerce/changelog/e2e-tag-e2e-tests | 4 + .../create-restricted-coupons.spec.js | 2 +- .../cart-block-calculate-shipping.spec.js | 282 +++++++++--------- .../tests/shopper/cart-block-coupons.spec.js | 218 ++++++++------ .../e2e-pw/tests/shopper/cart-block.spec.js | 224 +++++++------- .../shopper/cart-calculate-shipping.spec.js | 190 ++++++------ .../cart-checkout-block-calculate-tax.spec.js | 8 +- .../cart-checkout-calculate-tax.spec.js | 2 +- .../shopper/cart-checkout-coupons.spec.js | 95 +++--- .../cart-checkout-restricted-coupons.spec.js | 2 +- .../tests/shopper/cart-redirection.spec.js | 2 +- .../tests/e2e-pw/tests/shopper/cart.spec.js | 170 ++++++----- .../tests/shopper/launch-your-store.spec.js | 84 +++--- .../shopper/shop-title-after-deletion.spec.js | 2 +- .../tests/shopper/wordpress-post.spec.js | 2 +- 15 files changed, 684 insertions(+), 603 deletions(-) create mode 100644 plugins/woocommerce/changelog/e2e-tag-e2e-tests diff --git a/plugins/woocommerce/changelog/e2e-tag-e2e-tests b/plugins/woocommerce/changelog/e2e-tag-e2e-tests new file mode 100644 index 00000000000..4f73f5cc6c2 --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-tag-e2e-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + + 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 7a94c6deb02..dd6941c6cc9 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 @@ -96,7 +96,7 @@ const test = baseTest.extend( { }, } ); -test.describe( 'Restricted coupon management', { tag: '@services' }, () => { +test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => { for ( const couponType of Object.keys( couponData ) ) { test( `can create new ${ couponType } coupon`, async ( { page, diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-calculate-shipping.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-calculate-shipping.spec.js index 8649bd7e246..2b736893759 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-calculate-shipping.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-calculate-shipping.spec.js @@ -130,157 +130,173 @@ test.describe( } ); } ); - test( 'allows customer to calculate Free Shipping in cart block if in Netherlands', async ( { - page, - context, - cartBlockPage, - } ) => { - await context.clearCookies(); + test( + 'allows customer to calculate Free Shipping in cart block if in Netherlands', + { tag: [ '@could-be-unit-test' ] }, + async ( { page, context, cartBlockPage } ) => { + await context.clearCookies(); - await addAProductToCart( page, product1Id ); - await page.goto( cartBlockPage.slug ); + await addAProductToCart( page, product1Id ); + await page.goto( cartBlockPage.slug ); - // Set shipping country to Netherlands - await page.getByLabel( 'Add an address for shipping' ).click(); - await page - .getByRole( 'combobox' ) - .first() - .selectOption( 'Netherlands' ); - await page.getByLabel( 'Postal code' ).fill( '1011AA' ); - await page.getByLabel( 'City' ).fill( 'Amsterdam' ); - await page.getByRole( 'button', { name: 'Update' } ).click(); + // Set shipping country to Netherlands + await page.getByLabel( 'Add an address for shipping' ).click(); + await page + .getByRole( 'combobox' ) + .first() + .selectOption( 'Netherlands' ); + await page.getByLabel( 'Postal code' ).fill( '1011AA' ); + await page.getByLabel( 'City' ).fill( 'Amsterdam' ); + await page.getByRole( 'button', { name: 'Update' } ).click(); - // Verify shipping costs - await expect( - page.getByRole( 'group' ).getByText( 'Free shipping' ) - ).toBeVisible(); - await expect( - page.getByRole( 'strong' ).getByText( 'Free', { exact: true } ) - ).toBeVisible(); - await expect( page.getByText( '$' ).nth( 2 ) ).toContainText( - firstProductPrice - ); - } ); + // Verify shipping costs + await expect( + page.getByRole( 'group' ).getByText( 'Free shipping' ) + ).toBeVisible(); + await expect( + page + .getByRole( 'strong' ) + .getByText( 'Free', { exact: true } ) + ).toBeVisible(); + await expect( page.getByText( '$' ).nth( 2 ) ).toContainText( + firstProductPrice + ); + } + ); - test( 'allows customer to calculate Flat rate and Local pickup in cart block if in Portugal', async ( { - page, - context, - cartBlockPage, - } ) => { - await context.clearCookies(); + test( + 'allows customer to calculate Flat rate and Local pickup in cart block if in Portugal', + { tag: [ '@could-be-unit-test' ] }, + async ( { page, context, cartBlockPage } ) => { + await context.clearCookies(); - await addAProductToCart( page, product1Id ); - await page.goto( cartBlockPage.slug ); + await addAProductToCart( page, product1Id ); + await page.goto( cartBlockPage.slug ); - // Set shipping country to Portugal - await page.getByLabel( 'Add an address for shipping' ).click(); - await page - .getByRole( 'combobox' ) - .first() - .selectOption( 'Portugal' ); - await page.getByLabel( 'Postal code' ).fill( '1000-001' ); - await page.getByLabel( 'City' ).fill( 'Lisbon' ); - await page.getByRole( 'button', { name: 'Update' } ).click(); + // Set shipping country to Portugal + await page.getByLabel( 'Add an address for shipping' ).click(); + await page + .getByRole( 'combobox' ) + .first() + .selectOption( 'Portugal' ); + await page.getByLabel( 'Postal code' ).fill( '1000-001' ); + await page.getByLabel( 'City' ).fill( 'Lisbon' ); + await page.getByRole( 'button', { name: 'Update' } ).click(); - // Verify shipping costs - await expect( - page.getByRole( 'group' ).getByText( 'Flat rate' ) - ).toBeVisible(); - await expect( page.getByText( 'Shipping$5.00Flat' ) ).toBeVisible(); - await expect( - page.getByText( `$${ firstProductWithFlatRate }` ) - ).toBeVisible(); + // Verify shipping costs + await expect( + page.getByRole( 'group' ).getByText( 'Flat rate' ) + ).toBeVisible(); + await expect( + page.getByText( 'Shipping$5.00Flat' ) + ).toBeVisible(); + await expect( + page.getByText( `$${ firstProductWithFlatRate }` ) + ).toBeVisible(); - // Set shipping to local pickup instead of flat rate - await page.getByRole( 'group' ).getByText( 'Local pickup' ).click(); + // Set shipping to local pickup instead of flat rate + await page + .getByRole( 'group' ) + .getByText( 'Local pickup' ) + .click(); - // Verify updated shipping costs - await expect( page.getByText( 'ShippingFreeLocal' ) ).toBeVisible(); - await expect( page.getByText( '$' ).nth( 2 ) ).toContainText( - firstProductPrice - ); - } ); + // Verify updated shipping costs + await expect( + page.getByText( 'ShippingFreeLocal' ) + ).toBeVisible(); + await expect( page.getByText( '$' ).nth( 2 ) ).toContainText( + firstProductPrice + ); + } + ); - test( 'should show correct total cart block price after updating quantity', async ( { - page, - context, - cartBlockPage, - } ) => { - await context.clearCookies(); + test( + 'should show correct total cart block price after updating quantity', + { tag: [ '@could-be-unit-test' ] }, + async ( { page, context, cartBlockPage } ) => { + await context.clearCookies(); - await addAProductToCart( page, product1Id ); - await page.goto( cartBlockPage.slug ); + await addAProductToCart( page, product1Id ); + await page.goto( cartBlockPage.slug ); - // Set shipping country to Portugal - await page.getByLabel( 'Add an address for shipping' ).click(); - await page - .getByRole( 'combobox' ) - .first() - .selectOption( 'Portugal' ); - await page.getByLabel( 'Postal code' ).fill( '1000-001' ); - await page.getByLabel( 'City' ).fill( 'Lisbon' ); - await page.getByRole( 'button', { name: 'Update' } ).click(); + // Set shipping country to Portugal + await page.getByLabel( 'Add an address for shipping' ).click(); + await page + .getByRole( 'combobox' ) + .first() + .selectOption( 'Portugal' ); + await page.getByLabel( 'Postal code' ).fill( '1000-001' ); + await page.getByLabel( 'City' ).fill( 'Lisbon' ); + await page.getByRole( 'button', { name: 'Update' } ).click(); - // Increase product quantity and verify the updated price - await page.getByLabel( 'Increase quantity of First' ).click(); - await expect( - page.getByText( - `$${ - parseInt( firstProductPrice, 10 ) + - parseInt( firstProductPrice, 10 ) + - 5 - }`.toString() - ) - ).toBeVisible(); - } ); + // Increase product quantity and verify the updated price + await page.getByLabel( 'Increase quantity of First' ).click(); + await expect( + page.getByText( + `$${ + parseInt( firstProductPrice, 10 ) + + parseInt( firstProductPrice, 10 ) + + 5 + }`.toString() + ) + ).toBeVisible(); + } + ); - test( 'should show correct total cart block price with 2 different products and flat rate/local pickup', async ( { - page, - context, - cartBlockPage, - } ) => { - await context.clearCookies(); + test( + 'should show correct total cart block price with 2 different products and flat rate/local pickup', + { tag: [ '@could-be-unit-test' ] }, + async ( { page, context, cartBlockPage } ) => { + await context.clearCookies(); - await addAProductToCart( page, product1Id ); - await addAProductToCart( page, product2Id ); - await page.goto( cartBlockPage.slug ); + await addAProductToCart( page, product1Id ); + await addAProductToCart( page, product2Id ); + await page.goto( cartBlockPage.slug ); - // Set shipping country to Portugal - await page.getByLabel( 'Add an address for shipping' ).click(); - await page - .getByRole( 'combobox' ) - .first() - .selectOption( 'Portugal' ); - await page.getByLabel( 'Postal code' ).fill( '1000-001' ); - await page.getByLabel( 'City' ).fill( 'Lisbon' ); - await page.getByRole( 'button', { name: 'Update' } ).click(); + // Set shipping country to Portugal + await page.getByLabel( 'Add an address for shipping' ).click(); + await page + .getByRole( 'combobox' ) + .first() + .selectOption( 'Portugal' ); + await page.getByLabel( 'Postal code' ).fill( '1000-001' ); + await page.getByLabel( 'City' ).fill( 'Lisbon' ); + await page.getByRole( 'button', { name: 'Update' } ).click(); - // Verify shipping costs - await expect( - page.getByRole( 'group' ).getByText( 'Flat rate' ) - ).toBeVisible(); - await expect( page.getByText( 'Shipping$5.00Flat' ) ).toBeVisible(); - await expect( - page.getByText( - `$${ - parseInt( firstProductPrice, 10 ) + - parseInt( secondProductPrice, 10 ) + - 5 - }`.toString() - ) - ).toBeVisible(); + // Verify shipping costs + await expect( + page.getByRole( 'group' ).getByText( 'Flat rate' ) + ).toBeVisible(); + await expect( + page.getByText( 'Shipping$5.00Flat' ) + ).toBeVisible(); + await expect( + page.getByText( + `$${ + parseInt( firstProductPrice, 10 ) + + parseInt( secondProductPrice, 10 ) + + 5 + }`.toString() + ) + ).toBeVisible(); - // Set shipping to local pickup instead of flat rate - await page.getByRole( 'group' ).getByText( 'Local pickup' ).click(); + // Set shipping to local pickup instead of flat rate + await page + .getByRole( 'group' ) + .getByText( 'Local pickup' ) + .click(); - // Verify updated shipping costs - await expect( page.getByText( 'ShippingFreeLocal' ) ).toBeVisible(); - await expect( - page - .locator( 'div' ) - .filter( { hasText: /^\$30\.00$/ } ) - .locator( 'span' ) - ).toBeVisible(); - } ); + // Verify updated shipping costs + await expect( + page.getByText( 'ShippingFreeLocal' ) + ).toBeVisible(); + await expect( + page + .locator( 'div' ) + .filter( { hasText: /^\$30\.00$/ } ) + .locator( 'span' ) + ).toBeVisible(); + } + ); } ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-coupons.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-coupons.spec.js index 62d8d6f2cdc..9d709e40ea9 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-coupons.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-coupons.spec.js @@ -127,17 +127,111 @@ test.describe( } ); } ); - test( 'allows cart block to apply coupon of any type', async ( { - page, - } ) => { - const totals = [ '$50.00', '$27.50', '$45.00' ]; + test( + 'allows cart block to apply coupon of any type', + { tag: [ '@could-be-unit-test' ] }, + async ( { page } ) => { + const totals = [ '$50.00', '$27.50', '$45.00' ]; - // apply all coupon types - for ( let i = 0; i < coupons.length; i++ ) { + // apply all coupon types + for ( let i = 0; i < coupons.length; i++ ) { + await page + .getByRole( 'button', { name: 'Add a coupon' } ) + .click(); + await page + .getByLabel( 'Enter code' ) + .fill( coupons[ i ].code ); + await page.getByText( 'Apply', { exact: true } ).click(); + await expect( + page + .locator( + '.wc-block-components-notice-banner__content' + ) + .getByText( + `Coupon code "${ coupons[ i ].code }" has been applied to your cart.` + ) + ).toBeVisible(); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toHaveText( totals[ i ] ); + await page + .getByLabel( `Remove coupon "${ coupons[ i ].code }"` ) + .click(); + await expect( + page + .locator( + '.wc-block-components-notice-banner__content' + ) + .getByText( + `Coupon code "${ coupons[ i ].code }" has been removed from your cart.` + ) + ).toBeVisible(); + } + } + ); + + test( + 'allows cart block to apply multiple coupons', + { tag: [ '@could-be-unit-test' ] }, + async ( { page } ) => { + const totals = [ '$50.00', '$22.50', '$12.50' ]; + const totalsReverse = [ '$17.50', '$45.00', '$55.00' ]; + const discounts = [ '-$5.00', '-$32.50', '-$42.50' ]; + + // add all coupons and verify prices + for ( let i = 0; i < coupons.length; i++ ) { + await page + .getByRole( 'button', { name: 'Add a coupon' } ) + .click(); + await page + .getByLabel( 'Enter code' ) + .fill( coupons[ i ].code ); + await page.getByText( 'Apply', { exact: true } ).click(); + await expect( + page + .locator( + '.wc-block-components-notice-banner__content' + ) + .getByText( + `Coupon code "${ coupons[ i ].code }" has been applied to your cart.` + ) + ).toBeVisible(); + await expect( + page.locator( + '.wc-block-components-totals-discount > .wc-block-components-totals-item__value' + ) + ).toHaveText( discounts[ i ] ); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toHaveText( totals[ i ] ); + } + + for ( let i = 0; i < coupons.length; i++ ) { + await page + .getByLabel( `Remove coupon "${ coupons[ i ].code }"` ) + .click(); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toHaveText( totalsReverse[ i ] ); + } + } + ); + + test( + 'prevents cart block applying same coupon twice', + { tag: [ '@could-be-unit-test' ] }, + async ( { page } ) => { + // try to add two same coupons and verify the error message await page .getByRole( 'button', { name: 'Add a coupon' } ) .click(); - await page.getByLabel( 'Enter code' ).fill( coupons[ i ].code ); + await page.getByLabel( 'Enter code' ).fill( coupons[ 0 ].code ); await page.getByText( 'Apply', { exact: true } ).click(); await expect( page @@ -145,114 +239,40 @@ test.describe( '.wc-block-components-notice-banner__content' ) .getByText( - `Coupon code "${ coupons[ i ].code }" has been applied to your cart.` + `Coupon code "${ coupons[ 0 ].code }" has been applied to your cart.` ) ).toBeVisible(); - await expect( - page.locator( - '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' - ) - ).toHaveText( totals[ i ] ); - await page - .getByLabel( `Remove coupon "${ coupons[ i ].code }"` ) - .click(); - await expect( - page - .locator( - '.wc-block-components-notice-banner__content' - ) - .getByText( - `Coupon code "${ coupons[ i ].code }" has been removed from your cart.` - ) - ).toBeVisible(); - } - } ); - - test( 'allows cart block to apply multiple coupons', async ( { - page, - } ) => { - const totals = [ '$50.00', '$22.50', '$12.50' ]; - const totalsReverse = [ '$17.50', '$45.00', '$55.00' ]; - const discounts = [ '-$5.00', '-$32.50', '-$42.50' ]; - - // add all coupons and verify prices - for ( let i = 0; i < coupons.length; i++ ) { await page .getByRole( 'button', { name: 'Add a coupon' } ) .click(); - await page.getByLabel( 'Enter code' ).fill( coupons[ i ].code ); + await page.getByLabel( 'Enter code' ).fill( coupons[ 0 ].code ); await page.getByText( 'Apply', { exact: true } ).click(); await expect( page - .locator( - '.wc-block-components-notice-banner__content' - ) + .getByRole( 'alert' ) .getByText( - `Coupon code "${ coupons[ i ].code }" has been applied to your cart.` + `Coupon code "${ coupons[ 0 ].code }" has already been applied.` ) ).toBeVisible(); - await expect( - page.locator( - '.wc-block-components-totals-discount > .wc-block-components-totals-item__value' - ) - ).toHaveText( discounts[ i ] ); - await expect( - page.locator( - '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' - ) - ).toHaveText( totals[ i ] ); } + ); - for ( let i = 0; i < coupons.length; i++ ) { + test( + 'prevents cart block applying coupon with usage limit', + { tag: [ '@could-be-unit-test' ] }, + async ( { page } ) => { + // add coupon with usage limit await page - .getByLabel( `Remove coupon "${ coupons[ i ].code }"` ) + .getByRole( 'button', { name: 'Add a coupon' } ) .click(); + await page.getByLabel( 'Enter code' ).fill( couponLimitedCode ); + await page.getByText( 'Apply', { exact: true } ).click(); await expect( - page.locator( - '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' - ) - ).toHaveText( totalsReverse[ i ] ); + page + .getByRole( 'alert' ) + .getByText( 'Coupon usage limit has been reached.' ) + ).toBeVisible(); } - } ); - - test( 'prevents cart block applying same coupon twice', async ( { - page, - } ) => { - // try to add two same coupons and verify the error message - await page.getByRole( 'button', { name: 'Add a coupon' } ).click(); - await page.getByLabel( 'Enter code' ).fill( coupons[ 0 ].code ); - await page.getByText( 'Apply', { exact: true } ).click(); - await expect( - page - .locator( '.wc-block-components-notice-banner__content' ) - .getByText( - `Coupon code "${ coupons[ 0 ].code }" has been applied to your cart.` - ) - ).toBeVisible(); - await page.getByRole( 'button', { name: 'Add a coupon' } ).click(); - await page.getByLabel( 'Enter code' ).fill( coupons[ 0 ].code ); - await page.getByText( 'Apply', { exact: true } ).click(); - await expect( - page - .getByRole( 'alert' ) - .getByText( - `Coupon code "${ coupons[ 0 ].code }" has already been applied.` - ) - ).toBeVisible(); - } ); - - test( 'prevents cart block applying coupon with usage limit', async ( { - page, - } ) => { - // add coupon with usage limit - await page.getByRole( 'button', { name: 'Add a coupon' } ).click(); - await page.getByLabel( 'Enter code' ).fill( couponLimitedCode ); - await page.getByText( 'Apply', { exact: true } ).click(); - await expect( - page - .getByRole( 'alert' ) - .getByText( 'Coupon usage limit has been reached.' ) - ).toBeVisible(); - } ); + ); } ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block.spec.js index 11ab6abe7d9..3ce134b9e9c 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block.spec.js @@ -73,116 +73,132 @@ test.describe( 'Cart Block page', { tag: [ '@payments', '@services' ] }, () => { } ); } ); - test( 'can see empty cart, add and remove simple & cross sell product, increase to max quantity', async ( { - page, - testPage, - } ) => { - await goToPageEditor( { page } ); - await fillPageTitle( page, testPage.title ); - await insertBlockByShortcut( page, 'Cart' ); - await publishPage( page, testPage.title ); + test( + 'can see empty cart, add and remove simple & cross sell product, increase to max quantity', + { tag: [ '@could-be-unit-test' ] }, + async ( { page, testPage } ) => { + await goToPageEditor( { page } ); + await fillPageTitle( page, testPage.title ); + await insertBlockByShortcut( page, 'Cart' ); + await publishPage( page, testPage.title ); - // go to the page to test empty cart block - await page.goto( testPage.slug ); - await expect( - page.getByRole( 'heading', { name: testPage.title } ) - ).toBeVisible(); - await expect( - await page.getByText( 'Your cart is currently empty!' ).count() - ).toBeGreaterThan( 0 ); - await expect( - page.getByRole( 'link', { name: 'Browse store' } ) - ).toBeVisible(); - await page.getByRole( 'link', { name: 'Browse store' } ).click(); - await expect( - page.getByRole( 'heading', { name: 'Shop' } ) - ).toBeVisible(); + // go to the page to test empty cart block + await page.goto( testPage.slug ); + await expect( + page.getByRole( 'heading', { name: testPage.title } ) + ).toBeVisible(); + await expect( + await page.getByText( 'Your cart is currently empty!' ).count() + ).toBeGreaterThan( 0 ); + await expect( + page.getByRole( 'link', { name: 'Browse store' } ) + ).toBeVisible(); + await page.getByRole( 'link', { name: 'Browse store' } ).click(); + await expect( + page.getByRole( 'heading', { name: 'Shop' } ) + ).toBeVisible(); - await addAProductToCart( page, product1Id ); - await page.goto( testPage.slug ); - await expect( - page.getByRole( 'heading', { name: testPage.title } ) - ).toBeVisible(); - await expect( - page.getByRole( 'link', { name: simpleProductName, exact: true } ) - ).toBeVisible(); - await expect( page.getByText( simpleProductDesc ) ).toBeVisible(); - await expect( - page.getByText( `Save $${ singleProductSalePrice }` ) - ).toBeVisible(); + await addAProductToCart( page, product1Id ); + await page.goto( testPage.slug ); + await expect( + page.getByRole( 'heading', { name: testPage.title } ) + ).toBeVisible(); + await expect( + page.getByRole( 'link', { + name: simpleProductName, + exact: true, + } ) + ).toBeVisible(); + await expect( page.getByText( simpleProductDesc ) ).toBeVisible(); + await expect( + page.getByText( `Save $${ singleProductSalePrice }` ) + ).toBeVisible(); - // increase product quantity to its maximum - await expect( page.getByText( '2 left in stock' ) ).toBeVisible(); - await page - .getByRole( 'button' ) - .filter( { hasText: '+', exact: true } ) - .click(); - await expect( - page.locator( - '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' - ) - ).toContainText( `$${ doubleProductsPrice.toString() }` ); - await expect( - page.getByRole( 'button' ).filter( { hasText: '+', exact: true } ) - ).toBeDisabled(); + // increase product quantity to its maximum + await expect( page.getByText( '2 left in stock' ) ).toBeVisible(); + await page + .getByRole( 'button' ) + .filter( { hasText: '+', exact: true } ) + .click(); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toContainText( `$${ doubleProductsPrice.toString() }` ); + await expect( + page + .getByRole( 'button' ) + .filter( { hasText: '+', exact: true } ) + ).toBeDisabled(); - // add cross-sell products to cart - await expect( - page.getByRole( 'heading', { name: 'You may be interested in…' } ) - ).toBeVisible(); - await page - .getByLabel( `Add to cart: “${ simpleProductName } Cross-Sell 1”` ) - .click(); - await expect( - page - .locator( '.wc-block-cart-items' ) - .getByText( `${ simpleProductName } Cross-Sell 1` ) - ).toBeVisible(); - await page - .getByLabel( `Add to cart: “${ simpleProductName } Cross-Sell 2”` ) - .click(); - await expect( - page - .locator( '.wc-block-cart-items' ) - .getByText( `${ simpleProductName } Cross-Sell 2` ) - ).toBeVisible(); + // add cross-sell products to cart + await expect( + page.getByRole( 'heading', { + name: 'You may be interested in…', + } ) + ).toBeVisible(); + await page + .getByLabel( + `Add to cart: “${ simpleProductName } Cross-Sell 1”` + ) + .click(); + await expect( + page + .locator( '.wc-block-cart-items' ) + .getByText( `${ simpleProductName } Cross-Sell 1` ) + ).toBeVisible(); + await page + .getByLabel( + `Add to cart: “${ simpleProductName } Cross-Sell 2”` + ) + .click(); + await expect( + page + .locator( '.wc-block-cart-items' ) + .getByText( `${ simpleProductName } Cross-Sell 2` ) + ).toBeVisible(); - await page.goto( testPage.slug ); - await expect( - page.getByRole( 'heading', { name: testPage.title } ) - ).toBeVisible(); - await expect( - page.getByRole( 'heading', { name: 'You may be interested in…' } ) - ).toBeHidden(); - await expect( - page.locator( - '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' - ) - ).toContainText( - `$${ singleProductWithCrossSellProducts.toString() }` - ); + await page.goto( testPage.slug ); + await expect( + page.getByRole( 'heading', { name: testPage.title } ) + ).toBeVisible(); + await expect( + page.getByRole( 'heading', { + name: 'You may be interested in…', + } ) + ).toBeHidden(); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toContainText( + `$${ singleProductWithCrossSellProducts.toString() }` + ); - // remove cross-sell products from cart - await page.locator( ':nth-match(:text("Remove item"), 3)' ).click(); - await page.locator( ':nth-match(:text("Remove item"), 2)' ).click(); - await expect( - page.getByRole( 'heading', { name: 'You may be interested in…' } ) - ).toBeVisible(); + // remove cross-sell products from cart + await page.locator( ':nth-match(:text("Remove item"), 3)' ).click(); + await page.locator( ':nth-match(:text("Remove item"), 2)' ).click(); + await expect( + page.getByRole( 'heading', { + name: 'You may be interested in…', + } ) + ).toBeVisible(); - // check if the link to proceed to the checkout exists - await expect( - page.getByRole( 'link', { - name: 'Proceed to Checkout', - } ) - ).toBeVisible(); + // check if the link to proceed to the checkout exists + await expect( + page.getByRole( 'link', { + name: 'Proceed to Checkout', + } ) + ).toBeVisible(); - // remove product from cart - await page.locator( ':text("Remove item")' ).click(); - await expect( - page.getByText( 'Your cart is currently empty!' ) - ).toBeVisible(); - await expect( - page.getByRole( 'link', { name: 'Browse store' } ) - ).toBeVisible(); - } ); + // remove product from cart + await page.locator( ':text("Remove item")' ).click(); + await expect( + page.getByText( 'Your cart is currently empty!' ) + ).toBeVisible(); + await expect( + page.getByRole( 'link', { name: 'Browse store' } ) + ).toBeVisible(); + } + ); } ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-calculate-shipping.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-calculate-shipping.spec.js index 62be5e273a3..4f3d0026b88 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-calculate-shipping.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-calculate-shipping.spec.js @@ -125,108 +125,120 @@ test.describe( } ); } ); - test( 'allows customer to calculate Free Shipping if in Germany', async ( { - page, - } ) => { - await page.goto( '/cart/' ); - // Set shipping country to Germany - await page.locator( 'a.shipping-calculator-button' ).click(); - await page - .locator( '#calc_shipping_country' ) - .selectOption( shippingCountryDE ); - await page.locator( 'button[name="calc_shipping"]' ).click(); + test( + 'allows customer to calculate Free Shipping if in Germany', + { tag: [ '@could-be-unit-test' ] }, + async ( { page } ) => { + await page.goto( '/cart/' ); + // Set shipping country to Germany + await page.locator( 'a.shipping-calculator-button' ).click(); + await page + .locator( '#calc_shipping_country' ) + .selectOption( shippingCountryDE ); + await page.locator( 'button[name="calc_shipping"]' ).click(); - // Verify shipping costs - await expect( - page.locator( '.shipping ul#shipping_method > li' ) - ).toContainText( 'Free shipping' ); - await expect( - page.locator( '.order-total .amount' ) - ).toContainText( firstProductPrice ); - } ); + // Verify shipping costs + await expect( + page.locator( '.shipping ul#shipping_method > li' ) + ).toContainText( 'Free shipping' ); + await expect( + page.locator( '.order-total .amount' ) + ).toContainText( firstProductPrice ); + } + ); - test( 'allows customer to calculate Flat rate and Local pickup if in France', async ( { - page, - } ) => { - await page.goto( '/cart/' ); - // Set shipping country to France - await page.locator( 'a.shipping-calculator-button' ).click(); - await page - .locator( '#calc_shipping_country' ) - .selectOption( shippingCountryFR ); - await page.locator( 'button[name="calc_shipping"]' ).click(); + test( + 'allows customer to calculate Flat rate and Local pickup if in France', + { tag: [ '@could-be-unit-test' ] }, + async ( { page } ) => { + await page.goto( '/cart/' ); + // Set shipping country to France + await page.locator( 'a.shipping-calculator-button' ).click(); + await page + .locator( '#calc_shipping_country' ) + .selectOption( shippingCountryFR ); + await page.locator( 'button[name="calc_shipping"]' ).click(); - // Verify shipping costs - await expect( page.locator( '.shipping .amount' ) ).toContainText( - '$5.00' - ); - await expect( - page.locator( '.order-total .amount' ) - ).toContainText( `$${ firstProductWithFlatRate }` ); + // Verify shipping costs + await expect( + page.locator( '.shipping .amount' ) + ).toContainText( '$5.00' ); + await expect( + page.locator( '.order-total .amount' ) + ).toContainText( `$${ firstProductWithFlatRate }` ); - // Set shipping to local pickup instead of flat rate - await page.locator( 'text=Local pickup' ).click(); + // Set shipping to local pickup instead of flat rate + await page.locator( 'text=Local pickup' ).click(); - // Verify updated shipping costs - await expect( - page.locator( '.order-total .amount' ).first() - ).toContainText( `$${ firstProductPrice }` ); - } ); + // Verify updated shipping costs + await expect( + page.locator( '.order-total .amount' ).first() + ).toContainText( `$${ firstProductPrice }` ); + } + ); - test( 'should show correct total cart price after updating quantity', async ( { - page, - } ) => { - await page.goto( '/cart/' ); - await page.locator( 'input.qty' ).fill( '4' ); - await page.locator( 'text=Update cart' ).click(); + test( + 'should show correct total cart price after updating quantity', + { tag: [ '@could-be-unit-test' ] }, + async ( { page } ) => { + await page.goto( '/cart/' ); + await page.locator( 'input.qty' ).fill( '4' ); + await page.locator( 'text=Update cart' ).click(); - // Set shipping country to France - await page.locator( 'a.shipping-calculator-button' ).click(); - await page - .locator( '#calc_shipping_country' ) - .selectOption( shippingCountryFR ); - await page.locator( 'button[name="calc_shipping"]' ).click(); + // Set shipping country to France + await page.locator( 'a.shipping-calculator-button' ).click(); + await page + .locator( '#calc_shipping_country' ) + .selectOption( shippingCountryFR ); + await page.locator( 'button[name="calc_shipping"]' ).click(); - await expect( - page.locator( '.order-total .amount' ) - ).toContainText( `$${ fourProductsWithFlatRate }` ); - } ); + await expect( + page.locator( '.order-total .amount' ) + ).toContainText( `$${ fourProductsWithFlatRate }` ); + } + ); - test( 'should show correct total cart price with 2 products and flat rate', async ( { - page, - } ) => { - await addAProductToCart( page, secondProductId ); + test( + 'should show correct total cart price with 2 products and flat rate', + { tag: [ '@could-be-unit-test' ] }, + async ( { page } ) => { + await addAProductToCart( page, secondProductId ); - await page.goto( '/cart/' ); - await page.locator( 'a.shipping-calculator-button' ).click(); - await page - .locator( '#calc_shipping_country' ) - .selectOption( shippingCountryFR ); - await page.locator( 'button[name="calc_shipping"]' ).click(); + await page.goto( '/cart/' ); + await page.locator( 'a.shipping-calculator-button' ).click(); + await page + .locator( '#calc_shipping_country' ) + .selectOption( shippingCountryFR ); + await page.locator( 'button[name="calc_shipping"]' ).click(); - await expect( page.locator( '.shipping .amount' ) ).toContainText( - '$5.00' - ); - await expect( - page.locator( '.order-total .amount' ) - ).toContainText( `$${ twoProductsWithFlatRate }` ); - } ); + await expect( + page.locator( '.shipping .amount' ) + ).toContainText( '$5.00' ); + await expect( + page.locator( '.order-total .amount' ) + ).toContainText( `$${ twoProductsWithFlatRate }` ); + } + ); - test( 'should show correct total cart price with 2 products without flat rate', async ( { - page, - } ) => { - await addAProductToCart( page, secondProductId ); + test( + 'should show correct total cart price with 2 products without flat rate', + { tag: [ '@could-be-unit-test' ] }, + async ( { page } ) => { + await addAProductToCart( page, secondProductId ); - // Set shipping country to Spain - await page.goto( '/cart/' ); - await page.locator( 'a.shipping-calculator-button' ).click(); - await page.locator( '#calc_shipping_country' ).selectOption( 'ES' ); - await page.locator( 'button[name="calc_shipping"]' ).click(); + // Set shipping country to Spain + await page.goto( '/cart/' ); + await page.locator( 'a.shipping-calculator-button' ).click(); + await page + .locator( '#calc_shipping_country' ) + .selectOption( 'ES' ); + await page.locator( 'button[name="calc_shipping"]' ).click(); - // Verify shipping costs - await expect( - page.locator( '.order-total .amount' ) - ).toContainText( `$${ twoProductsTotal }` ); - } ); + // Verify shipping costs + await expect( + page.locator( '.order-total .amount' ) + ).toContainText( `$${ twoProductsTotal }` ); + } + ); } ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-block-calculate-tax.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-block-calculate-tax.spec.js index f449ebe1b3a..eb189618973 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-block-calculate-tax.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-block-calculate-tax.spec.js @@ -38,7 +38,7 @@ let productId, test.describe( 'Shopper Cart & Checkout Block Tax Display', - { tag: [ '@payments', '@services', '@hpos' ] }, + { tag: [ '@payments', '@services', '@hpos', '@could-be-unit-test' ] }, () => { test.use( { storageState: process.env.ADMINSTATE } ); test.beforeAll( async ( { baseURL } ) => { @@ -240,7 +240,7 @@ test.describe( test.describe( 'Shopper Cart & Checkout Block Tax Rounding', - { tag: [ '@payments', '@services' ] }, + { tag: [ '@payments', '@services', '@could-be-unit-test' ] }, () => { test.beforeAll( async ( { baseURL } ) => { const api = new wcApi( { @@ -484,7 +484,7 @@ test.describe( test.describe( 'Shopper Cart & Checkout Block Tax Levels', - { tag: [ '@payments', '@services' ] }, + { tag: [ '@payments', '@services', '@could-be-unit-test' ] }, () => { test.beforeAll( async ( { baseURL } ) => { const api = new wcApi( { @@ -809,7 +809,7 @@ test.describe( test.describe( 'Shipping Cart & Checkout Block Tax', - { tag: [ '@payments', '@services' ] }, + { tag: [ '@payments', '@services', '@could-be-unit-test' ] }, () => { test.beforeAll( async ( { baseURL } ) => { const api = new wcApi( { diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-calculate-tax.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-calculate-tax.spec.js index bfcc1218a69..f5f1b416487 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-calculate-tax.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-calculate-tax.spec.js @@ -24,7 +24,7 @@ let productId, test.describe.serial( 'Tax rates in the cart and checkout', - { tag: [ '@payments', '@services', '@hpos' ] }, + { tag: [ '@payments', '@services', '@hpos', '@could-be-unit-test' ] }, () => { test.beforeAll( async ( { baseURL } ) => { const api = new wcApi( { diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-coupons.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-coupons.spec.js index 813b3623236..6301a750e90 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-coupons.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-coupons.spec.js @@ -306,60 +306,65 @@ test.describe( } ); } ); - test( 'restores total when coupons are removed', async ( { - page, - context, - } ) => { - await test.step( 'Load cart page and try restoring total when removed coupons', async () => { - await addAProductToCart( page, firstProductId ); + test( + 'restores total when coupons are removed', + { tag: [ '@could-be-unit-test' ] }, + async ( { page, context } ) => { + await test.step( 'Load cart page and try restoring total when removed coupons', async () => { + await addAProductToCart( page, firstProductId ); - await page.goto( '/cart/' ); - await page.locator( '#coupon_code' ).fill( coupons[ 0 ].code ); - await page - .getByRole( 'button', { name: 'Apply coupon' } ) - .click(); + await page.goto( '/cart/' ); + await page + .locator( '#coupon_code' ) + .fill( coupons[ 0 ].code ); + await page + .getByRole( 'button', { name: 'Apply coupon' } ) + .click(); - // confirm numbers - await expect( - page.locator( '.cart-discount .amount' ) - ).toContainText( discounts[ 0 ] ); - await expect( - page.locator( '.order-total .amount' ) - ).toContainText( totals[ 0 ] ); + // confirm numbers + await expect( + page.locator( '.cart-discount .amount' ) + ).toContainText( discounts[ 0 ] ); + await expect( + page.locator( '.order-total .amount' ) + ).toContainText( totals[ 0 ] ); - await page.locator( 'a.woocommerce-remove-coupon' ).click(); + await page.locator( 'a.woocommerce-remove-coupon' ).click(); - await expect( - page.locator( '.order-total .amount' ) - ).toContainText( '$20.00' ); - } ); + await expect( + page.locator( '.order-total .amount' ) + ).toContainText( '$20.00' ); + } ); - await context.clearCookies(); + await context.clearCookies(); - await test.step( 'Load checkout page and try restoring total when removed coupons', async () => { - await addAProductToCart( page, firstProductId ); + await test.step( 'Load checkout page and try restoring total when removed coupons', async () => { + await addAProductToCart( page, firstProductId ); - await page.goto( '/checkout/' ); - await page - .locator( 'text=Click here to enter your code' ) - .click(); - await page.locator( '#coupon_code' ).fill( coupons[ 0 ].code ); - await page.locator( 'text=Apply coupon' ).click(); + await page.goto( '/checkout/' ); + await page + .locator( 'text=Click here to enter your code' ) + .click(); + await page + .locator( '#coupon_code' ) + .fill( coupons[ 0 ].code ); + await page.locator( 'text=Apply coupon' ).click(); - // confirm numbers - await expect( - page.locator( '.cart-discount .amount' ) - ).toContainText( discounts[ 0 ] ); - await expect( - page.locator( '.order-total .amount' ) - ).toContainText( totals[ 0 ] ); + // confirm numbers + await expect( + page.locator( '.cart-discount .amount' ) + ).toContainText( discounts[ 0 ] ); + await expect( + page.locator( '.order-total .amount' ) + ).toContainText( totals[ 0 ] ); - await page.locator( 'a.woocommerce-remove-coupon' ).click(); + await page.locator( 'a.woocommerce-remove-coupon' ).click(); - await expect( - page.locator( '.order-total .amount' ) - ).toContainText( '$20.00' ); - } ); - } ); + await expect( + page.locator( '.order-total .amount' ) + ).toContainText( '$20.00' ); + } ); + } + ); } ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-restricted-coupons.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-restricted-coupons.spec.js index 172c1e845a5..8ee5ca1d5e3 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-restricted-coupons.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-restricted-coupons.spec.js @@ -17,7 +17,7 @@ const awaitCartPageResponse = ( page ) => test.describe( 'Cart & Checkout Restricted Coupons', - { tag: [ '@payments', '@services', '@hpos' ] }, + { tag: [ '@payments', '@services', '@hpos', '@could-be-unit-test' ] }, () => { let firstProductId, secondProductId, diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-redirection.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-redirection.spec.js index 869a9cd0c5f..186b7965e3e 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-redirection.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-redirection.spec.js @@ -3,7 +3,7 @@ const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; test.describe( 'Cart > Redirect to cart from shop', - { tag: [ '@payments', '@services' ] }, + { tag: [ '@payments', '@services', '@not-e2e' ] }, () => { let productId; const productName = 'A redirect product test'; diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart.spec.js index f1dcdeeebd5..58b7a58c68f 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart.spec.js @@ -81,100 +81,116 @@ test.describe( 'Cart page', { tag: [ '@payments', '@services' ] }, () => { await responsePromise; } - test( 'should display no item in the cart', async ( { page } ) => { - await page.goto( '/cart/' ); - await expect( - page.getByText( 'Your cart is currently empty.' ) - ).toBeVisible(); - } ); - - test( 'should add the product to the cart from the shop page', async ( { - page, - } ) => { - await goToShopPageAndAddProductToCart( page, productName ); - - await page.goto( '/cart/' ); - await expect( page.locator( 'td.product-name' ) ).toContainText( - productName - ); - } ); - - test( 'should increase item quantity when "Add to cart" of the same product is clicked', async ( { - page, - } ) => { - let qty = 2; - while ( qty-- ) { - await goToShopPageAndAddProductToCart( page, productName ); + test( + 'should display no item in the cart', + { tag: [ '@could-be-unit-test' ] }, + async ( { page } ) => { + await page.goto( '/cart/' ); + await expect( + page.getByText( 'Your cart is currently empty.' ) + ).toBeVisible(); } + ); - await page.goto( '/cart/' ); - await expect( page.locator( 'input.qty' ) ).toHaveValue( '2' ); - } ); + test( + 'should add the product to the cart from the shop page', + { tag: [ '@could-be-unit-test' ] }, + async ( { page } ) => { + await goToShopPageAndAddProductToCart( page, productName ); - test( 'should update quantity when updated via quantity input', async ( { - page, - } ) => { - await goToShopPageAndAddProductToCart( page, productName ); + await page.goto( '/cart/' ); + await expect( page.locator( 'td.product-name' ) ).toContainText( + productName + ); + } + ); - await page.goto( '/cart/' ); - await page.locator( 'input.qty' ).fill( '2' ); - await page.locator( 'text=Update cart' ).click(); + test( + 'should increase item quantity when "Add to cart" of the same product is clicked', + { tag: [ '@could-be-unit-test' ] }, + async ( { page } ) => { + let qty = 2; + while ( qty-- ) { + await goToShopPageAndAddProductToCart( page, productName ); + } - await expect( page.locator( '.order-total .amount' ) ).toContainText( - `$${ twoProductPrice }` - ); - } ); + await page.goto( '/cart/' ); + await expect( page.locator( 'input.qty' ) ).toHaveValue( '2' ); + } + ); - test( 'should remove the item from the cart when remove is clicked', async ( { - page, - } ) => { - await goToShopPageAndAddProductToCart( page, productName ); - await page.goto( '/cart/' ); + test( + 'should update quantity when updated via quantity input', + { tag: [ '@could-be-unit-test' ] }, + async ( { page } ) => { + await goToShopPageAndAddProductToCart( page, productName ); - // make sure that the product is in the cart - await expect( page.locator( '.order-total .amount' ) ).toContainText( - `$${ productPrice }` - ); + await page.goto( '/cart/' ); + await page.locator( 'input.qty' ).fill( '2' ); + await page.locator( 'text=Update cart' ).click(); - await page.locator( 'a.remove' ).click(); + await expect( + page.locator( '.order-total .amount' ) + ).toContainText( `$${ twoProductPrice }` ); + } + ); - await expect( - page.getByText( `“${ productName }” removed` ) - ).toBeVisible(); - await expect( - page.getByText( 'Your cart is currently empty' ) - ).toBeVisible(); - } ); + test( + 'should remove the item from the cart when remove is clicked', + { tag: [ '@could-be-unit-test' ] }, + async ( { page } ) => { + await goToShopPageAndAddProductToCart( page, productName ); + await page.goto( '/cart/' ); - test( 'should update subtotal in cart totals when adding product to the cart', async ( { - page, - } ) => { - await goToShopPageAndAddProductToCart( page, productName ); + // make sure that the product is in the cart + await expect( + page.locator( '.order-total .amount' ) + ).toContainText( `$${ productPrice }` ); - await page.goto( '/cart/' ); - await expect( page.locator( '.cart-subtotal .amount' ) ).toContainText( - `$${ productPrice }` - ); + await page.locator( 'a.remove' ).click(); - await page.locator( 'input.qty' ).fill( '2' ); - await page.locator( 'text=Update cart' ).click(); + await expect( + page.getByText( `“${ productName }” removed` ) + ).toBeVisible(); + await expect( + page.getByText( 'Your cart is currently empty' ) + ).toBeVisible(); + } + ); - await expect( page.locator( '.order-total .amount' ) ).toContainText( - `$${ twoProductPrice }` - ); - } ); + test( + 'should update subtotal in cart totals when adding product to the cart', + { tag: [ '@could-be-unit-test' ] }, + async ( { page } ) => { + await goToShopPageAndAddProductToCart( page, productName ); - test( 'should go to the checkout page when "Proceed to Checkout" is clicked', async ( { - page, - } ) => { - await goToShopPageAndAddProductToCart( page, productName ); + await page.goto( '/cart/' ); + await expect( + page.locator( '.cart-subtotal .amount' ) + ).toContainText( `$${ productPrice }` ); - await page.goto( '/cart/' ); + await page.locator( 'input.qty' ).fill( '2' ); + await page.locator( 'text=Update cart' ).click(); - await page.locator( '.checkout-button' ).click(); + await expect( + page.locator( '.order-total .amount' ) + ).toContainText( `$${ twoProductPrice }` ); + } + ); - await expect( page.locator( '#order_review' ) ).toBeVisible(); - } ); + test( + 'should go to the checkout page when "Proceed to Checkout" is clicked', + { tag: [ '@could-be-unit-test' ] }, + async ( { page } ) => { + await goToShopPageAndAddProductToCart( page, productName ); + + await page.goto( '/cart/' ); + + await page.locator( '.checkout-button' ).click(); + + await expect( page.locator( '#order_review' ) ).toBeVisible(); + } + ); test( 'can manage cross-sell products and maximum item quantity', async ( { page, diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/launch-your-store.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/launch-your-store.spec.js index 0953328ce3a..5f85238a598 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/launch-your-store.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/launch-your-store.spec.js @@ -72,51 +72,43 @@ async function runComingSoonTests( themeContext = '' ) { } ); } -test.describe( - 'Launch Your Store front end - logged out', - { tag: [ '@payments', '@services' ] }, - () => { - test.afterAll( async ( { baseURL } ) => { - try { - await setOption( - request, - baseURL, - 'woocommerce_coming_soon', - 'no' - ); - } catch ( error ) { - console.log( error ); - } - } ); - - test.describe( 'Block Theme (Twenty Twenty Four)', () => { - test.beforeAll( async () => { - await activateTheme( 'twentytwentyfour' ); - } ); - - test.afterAll( async () => { - // Reset theme to the default. - await activateTheme( DEFAULT_THEME ); - } ); - - runComingSoonTests( test.step, test.use ); - } ); - - test.describe( 'Classic Theme (Storefront)', () => { - test.beforeAll( async () => { - await activateTheme( 'storefront' ); - } ); - - test.afterAll( async () => { - // Reset theme to the default. - await activateTheme( DEFAULT_THEME ); - } ); - - runComingSoonTests( - test.step, - test.use, - 'Classic Theme (Storefront)' +test.describe( 'Launch Your Store front end - logged out', () => { + test.afterAll( async ( { baseURL } ) => { + try { + await setOption( + request, + baseURL, + 'woocommerce_coming_soon', + 'no' ); + } catch ( error ) { + console.log( error ); + } + } ); + + test.describe( 'Block Theme (Twenty Twenty Four)', () => { + test.beforeAll( async () => { + await activateTheme( 'twentytwentyfour' ); } ); - } -); + + test.afterAll( async () => { + // Reset theme to the default. + await activateTheme( DEFAULT_THEME ); + } ); + + runComingSoonTests( test.step, test.use ); + } ); + + test.describe( 'Classic Theme (Storefront)', () => { + test.beforeAll( async () => { + await activateTheme( 'storefront' ); + } ); + + test.afterAll( async () => { + // Reset theme to the default. + await activateTheme( DEFAULT_THEME ); + } ); + + runComingSoonTests( test.step, test.use, 'Classic Theme (Storefront)' ); + } ); +} ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/shop-title-after-deletion.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/shop-title-after-deletion.spec.js index d967cff9d13..5c361b1ed50 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/shop-title-after-deletion.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/shop-title-after-deletion.spec.js @@ -3,7 +3,7 @@ const { test, expect } = require( '@playwright/test' ); // test case for bug https://github.com/woocommerce/woocommerce/pull/46429 test.describe( 'Check the title of the shop page after the page has been deleted', - { tag: [ '@payments', '@services' ] }, + { tag: [ '@payments', '@services', '@could-be-unit-test' ] }, () => { test.use( { storageState: process.env.ADMINSTATE } ); test.beforeEach( async ( { page } ) => { diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/wordpress-post.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/wordpress-post.spec.js index 6a1baa7c95d..fd6f46e0497 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/wordpress-post.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/wordpress-post.spec.js @@ -6,7 +6,7 @@ const test = baseTest.extend( { test( 'logged-in customer can comment on a post', - { tag: [ '@gutenberg', '@payments', '@services' ] }, + { tag: [ '@non-critical' ] }, async ( { page } ) => { await page.goto( 'hello-world/' ); await expect( From 8b35b0785c761bda0b90188ade462c83af5b3643 Mon Sep 17 00:00:00 2001 From: Sam Najian Date: Mon, 2 Sep 2024 14:23:52 +0200 Subject: [PATCH 011/187] Update woocommerce shipping promo banner [wc-shipping-188] (#50970) * Make sure the WC Shipping slug is used for installation * Make sure the check to show banner metabox work for HPOS as well * Make ShippingLabelBannerDisplayRules::order_has_shippable_products work with HPOS as well * Remove Jetpack plugin specific checks in ShippingLabelBannerDisplayRules * Use correct variable names for dotcom connection * Fix comments * Remove depenency on WCS&T for showing WC Shipping promo banner * Remove WC Tax and WC Shipping from incompatible plugins * Vary action button label if WCS&T is already installed * Inject config and render label purchase app after activation * Open the purchase modal after adding it to DOM * Render Shipment tracking metabox * Use a different headline when WCS&T is already installed * Fix UX when a none-compatible WCS&T is already active * Fix CSS linting issues * Fix Jslint issues * Improve around usage of localized variables * Fix and update JS tests * Address phpcs issues * Delete metaboxes of compatible WCS&T * Remove redundant variable assignment * Remove css and js of WCS&T if a compatible version is installed * Fix failing legacy PHPUnit tests * Only open the new label purchase modal if WCS&T is not active * Remove redundant code around TOS acceptance for showing the banner * Remove redundant test for Jetpack version checking * Make sure target passed to MutationObserver.observe is available * Add changelog file * Add openWcsModal to component's prototype * Add more js unit tests * Address PHP notice * Remove redundant variable assignments * Rename wcsPluginSlug to more clear wcShippingPluginSlug * Add a link to plugins page if incompatible WCS&T is already installed * Remove unused function parameters * Fix API resource path * Handle a case where none compatible version of WCShipping is installed --- .../shipping-banner/index.js | 384 ++++++++++-------- .../shipping-banner/test/index.js | 290 +++++++++---- .../print-shipping-label-banner/style.scss | 10 + .../print-shipping-label-banner/wcs-api.js | 12 +- ...date-woocommerce-shipping-promo-banner-188 | 4 + .../Internal/Admin/ShippingLabelBanner.php | 68 ++-- .../Admin/ShippingLabelBannerDisplayRules.php | 103 +---- ...ts-shipping-label-banner-display-rules.php | 86 +--- 8 files changed, 514 insertions(+), 443 deletions(-) create mode 100644 plugins/woocommerce/changelog/update-woocommerce-shipping-promo-banner-188 diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/print-shipping-label-banner/shipping-banner/index.js b/plugins/woocommerce-admin/client/wp-admin-scripts/print-shipping-label-banner/shipping-banner/index.js index 4a41285e9cd..1a29de7cc3e 100644 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/print-shipping-label-banner/shipping-banner/index.js +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/print-shipping-label-banner/shipping-banner/index.js @@ -1,17 +1,17 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; import { Button, ExternalLink } from '@wordpress/components'; import { compose } from '@wordpress/compose'; import interpolateComponents from '@automattic/interpolate-components'; import PropTypes from 'prop-types'; -import { get, isArray } from 'lodash'; import { PLUGINS_STORE_NAME } from '@woocommerce/data'; import { withDispatch, withSelect } from '@wordpress/data'; import { recordEvent } from '@woocommerce/tracks'; -import { getSetting } from '@woocommerce/settings'; +import { getSetting, getAdminLink } from '@woocommerce/settings'; +import { Link } from '@woocommerce/components'; /** * Internal dependencies @@ -19,29 +19,28 @@ import { getSetting } from '@woocommerce/settings'; import '../style.scss'; import DismissModal from '../dismiss-modal'; import SetupNotice, { setupErrorTypes } from '../setup-notice'; -import { getWcsAssets, acceptWcsTos } from '../wcs-api'; +import { + getWcsAssets, + acceptWcsTos, + getWcsLabelPurchaseConfigs, +} from '../wcs-api'; const wcAssetUrl = getSetting( 'wcAssetUrl', '' ); -const wcsPluginSlug = 'woocommerce-services'; +const wcShippingPluginSlug = 'woocommerce-shipping'; +const wcstPluginSlug = 'woocommerce-services'; export class ShippingBanner extends Component { constructor( props ) { super( props ); - const orderId = new URL( window.location.href ).searchParams.get( - 'post' - ); - this.state = { showShippingBanner: true, isDismissModalOpen: false, setupErrorReason: setupErrorTypes.SETUP, - orderId: parseInt( orderId, 10 ), wcsAssetsLoaded: false, wcsAssetsLoading: false, wcsSetupError: false, isShippingLabelButtonBusy: false, - installText: this.getInstallText(), isWcsModalOpen: false, }; } @@ -76,8 +75,8 @@ export class ShippingBanner extends Component { const { activePlugins } = this.props; this.setState( { isShippingLabelButtonBusy: true } ); this.trackElementClicked( 'shipping_banner_create_label' ); - if ( ! activePlugins.includes( wcsPluginSlug ) ) { - this.installAndActivatePlugins( wcsPluginSlug ); + if ( ! activePlugins.includes( wcShippingPluginSlug ) ) { + this.installAndActivatePlugins( wcShippingPluginSlug ); } else { this.acceptTosAndGetWCSAssets(); } @@ -85,7 +84,14 @@ export class ShippingBanner extends Component { async installAndActivatePlugins( pluginSlug ) { // Avoid double activating. - const { installPlugins, activatePlugins, isRequesting } = this.props; + const { + installPlugins, + activatePlugins, + isRequesting, + activePlugins, + isWcstCompatible, + isIncompatibleWCShippingInstalled, + } = this.props; if ( isRequesting ) { return false; } @@ -107,7 +113,25 @@ export class ShippingBanner extends Component { return; } - this.acceptTosAndGetWCSAssets(); + /** + * If a incompatible version of the WooCommerce Shipping plugin is installed, the necessary endpoints + * are not available, so we need to reload the page to ensure to make the plugin usable. + */ + if ( isIncompatibleWCShippingInstalled ) { + window.location.reload( true ); + return; + } + + if ( + ! activePlugins.includes( wcShippingPluginSlug ) && + isWcstCompatible + ) { + this.acceptTosAndGetWCSAssets(); + } else { + this.setState( { + showShippingBanner: false, + } ); + } } woocommerceServiceLinkClicked = () => { @@ -120,7 +144,7 @@ export class ShippingBanner extends Component { banner_name: 'wcadmin_install_wcs_prompt', jetpack_installed: activePlugins.includes( 'jetpack' ), jetpack_connected: isJetpackConnected, - wcs_installed: activePlugins.includes( wcsPluginSlug ), + wcs_installed: activePlugins.includes( wcShippingPluginSlug ), ...customProps, } ); }; @@ -135,16 +159,21 @@ export class ShippingBanner extends Component { } ); }; - acceptTosAndGetWCSAssets() { + acceptTosAndGetWCSAssets = () => { return acceptWcsTos() + .then( () => getWcsLabelPurchaseConfigs( this.props.orderId ) ) + .then( ( configs ) => { + window.WCShipping_Config = configs.config; + return configs; + } ) .then( () => getWcsAssets() ) .then( ( wcsAssets ) => this.loadWcsAssets( wcsAssets ) ) - .catch( () => this.setState( { wcsSetupError: true } ) ); - } + .catch( () => { + this.setState( { wcsSetupError: true } ); + } ); + }; generateMetaBoxHtml( nodeId, title, args ) { - const argsJsonString = JSON.stringify( args ).replace( /"/g, '"' ); // JS has no native html_entities so we just replace. - const togglePanelText = __( 'Toggle panel:', 'woocommerce' ); return ` @@ -159,8 +188,7 @@ export class ShippingBanner extends Component {
-
-
+
`; @@ -174,27 +202,24 @@ export class ShippingBanner extends Component { this.setState( { wcsAssetsLoading: true } ); - const jsPath = assets.wc_connect_admin_script; - const stylePath = assets.wc_connect_admin_style; + const labelPurchaseMetaboxId = 'woocommerce-order-label'; + const shipmentTrackingMetaboxId = 'woocommerce-order-shipment-tracking'; + const jsPath = assets.wcshipping_create_label_script; + const stylePath = assets.wcshipping_create_label_style; - if ( undefined === window.wcsPluginData ) { - const assetPath = jsPath.substring( - 0, - jsPath.lastIndexOf( '/' ) + 1 - ); - window.wcsPluginData = { assetPath }; - } + const shipmentTrackingJsPath = + assets.wcshipping_shipment_tracking_script; + const shipmentTrackingStylePath = + assets.wcshipping_shipment_tracking_style; - const { orderId } = this.state; - const { itemsCount } = this.props; + const { activePlugins } = this.props; + document.getElementById( labelPurchaseMetaboxId )?.remove(); const shippingLabelContainerHtml = this.generateMetaBoxHtml( - 'woocommerce-order-label', + labelPurchaseMetaboxId, __( 'Shipping Label', 'woocommerce' ), { - order: { id: orderId }, context: 'shipping_label', - items: itemsCount, } ); // Insert shipping label metabox just above main order details box. @@ -202,13 +227,12 @@ export class ShippingBanner extends Component { .getElementById( 'woocommerce-order-data' ) .insertAdjacentHTML( 'beforebegin', shippingLabelContainerHtml ); + document.getElementById( shipmentTrackingMetaboxId )?.remove(); const shipmentTrackingHtml = this.generateMetaBoxHtml( - 'woocommerce-order-shipment-tracking', + shipmentTrackingMetaboxId, __( 'Shipment Tracking', 'woocommerce' ), { - order: { id: orderId }, context: 'shipment_tracking', - items: itemsCount, } ); // Insert tracking metabox in the side after the order actions. @@ -224,6 +248,13 @@ export class ShippingBanner extends Component { window.jQuery( '#woocommerce-order-label' ).hide(); } + document + .querySelectorAll( 'script[src*="/woocommerce-services/"]' ) + .forEach( ( node ) => node.remove?.() ); + document + .querySelectorAll( 'link[href*="/woocommerce-services/"]' ) + .forEach( ( node ) => node.remove?.() ); + Promise.all( [ new Promise( ( resolve, reject ) => { const script = document.createElement( 'script' ); @@ -233,9 +264,16 @@ export class ShippingBanner extends Component { script.onerror = reject; document.body.appendChild( script ); } ), + new Promise( ( resolve, reject ) => { + const script = document.createElement( 'script' ); + script.src = shipmentTrackingJsPath; + script.async = true; + script.onload = resolve; + script.onerror = reject; + document.body.appendChild( script ); + } ), new Promise( ( resolve, reject ) => { if ( stylePath !== '' ) { - const head = document.getElementsByTagName( 'head' )[ 0 ]; const link = document.createElement( 'link' ); link.rel = 'stylesheet'; link.type = 'text/css'; @@ -243,7 +281,23 @@ export class ShippingBanner extends Component { link.media = 'all'; link.onload = resolve; link.onerror = reject; - head.appendChild( link ); + link.id = 'wcshipping-injected-styles'; + document.head.appendChild( link ); + } else { + resolve(); + } + } ), + new Promise( ( resolve, reject ) => { + if ( shipmentTrackingStylePath !== '' ) { + const link = document.createElement( 'link' ); + link.rel = 'stylesheet'; + link.type = 'text/css'; + link.href = shipmentTrackingStylePath; + link.media = 'all'; + link.onload = resolve; + link.onerror = reject; + link.id = 'wcshipping-injected-styles'; + document.head.appendChild( link ); } else { resolve(); } @@ -254,133 +308,61 @@ export class ShippingBanner extends Component { wcsAssetsLoading: false, isShippingLabelButtonBusy: false, } ); - this.openWcsModal(); + + // Reshow the shipping label metabox. + if ( window.jQuery ) { + window.jQuery( '#woocommerce-order-label' ).show(); + } + + document.getElementById( + 'woocommerce-admin-print-label' + ).style.display = 'none'; + + /** + * We'll only get to this point if either WCS&T is not active or is active but compatible with WooCommerce Shipping + * so once we check if the WCS&T is not active, we can open the label purchase modal immediately. + */ + if ( ! activePlugins.includes( wcstPluginSlug ) ) { + this.openWcsModal(); + } } ); } - getInstallText = () => { - const { activePlugins } = this.props; - if ( activePlugins.includes( wcsPluginSlug ) ) { - // If WCS is active, then the only remaining step is to agree to the ToS. - return __( - 'You\'ve already installed WooCommerce Shipping. By clicking "Create shipping label", you agree to its {{tosLink}}Terms of Service{{/tosLink}}.', - 'woocommerce' - ); - } - return __( - 'By clicking "Create shipping label", {{wcsLink}}WooCommerce Shipping{{/wcsLink}} will be installed and you agree to its {{tosLink}}Terms of Service{{/tosLink}}.', - 'woocommerce' - ); - }; - openWcsModal() { - if ( window.wcsGetAppStoreAsync ) { - window - .wcsGetAppStoreAsync( 'wc-connect-create-shipping-label' ) - .then( ( wcsStore ) => { - const state = wcsStore.getState(); - const { orderId } = this.state; - const siteId = state.ui.selectedSiteId; + // Since the button is dynamically added, we need to wait for it to become selectable and then click it. - const wcsStoreUnsubscribe = wcsStore.subscribe( () => { - const latestState = wcsStore.getState(); + const buttonSelector = + '#woocommerce-shipping-shipping-label-shipping_label button'; + if ( window.MutationObserver ) { + const observer = new window.MutationObserver( + ( mutationsList, observing ) => { + const button = document.querySelector( buttonSelector ); + if ( button ) { + button.click(); + observing.disconnect(); + } + } + ); - const shippingLabelState = get( - latestState, - [ - 'extensions', - 'woocommerce', - 'woocommerceServices', - siteId, - 'shippingLabel', - orderId, - ], - null - ); - - const labelSettingsState = get( - latestState, - [ - 'extensions', - 'woocommerce', - 'woocommerceServices', - siteId, - 'labelSettings', - ], - null - ); - - const packageState = get( - latestState, - [ - 'extensions', - 'woocommerce', - 'woocommerceServices', - siteId, - 'packages', - ], - null - ); - - const locationsState = get( latestState, [ - 'extensions', - 'woocommerce', - 'sites', - siteId, - 'data', - 'locations', - ] ); - - if ( - shippingLabelState && - labelSettingsState && - labelSettingsState.meta && - packageState && - locationsState - ) { - if ( - shippingLabelState.loaded && - labelSettingsState.meta.isLoaded && - packageState.isLoaded && - isArray( locationsState ) && - ! this.state.isWcsModalOpen - ) { - if ( window.jQuery ) { - this.setState( { isWcsModalOpen: true } ); - window - .jQuery( - '.shipping-label__new-label-button' - ) - .click(); - } - wcsStore.dispatch( { - type: 'NOTICE_CREATE', - notice: { - duration: 10000, - status: 'is-success', - text: __( - 'Plugin installed and activated', - 'woocommerce' - ), - }, - } ); - } else if ( - shippingLabelState.showPurchaseDialog - ) { - wcsStoreUnsubscribe(); - if ( window.jQuery ) { - window - .jQuery( '#woocommerce-order-label' ) - .show(); - } - } - } - } ); - - document.getElementById( - 'woocommerce-admin-print-label' - ).style.display = 'none'; - } ); + observer.observe( + document.getElementById( + 'woocommerce-shipping-shipping-label-shipping_label' + ) ?? + document.getElementById( 'wpbody-content' ) ?? + document.body, + { + childList: true, + subtree: true, + } + ); + } else { + const interval = setInterval( () => { + const targetElement = document.querySelector( buttonSelector ); + if ( targetElement ) { + targetElement.click(); + clearInterval( interval ); + } + }, 300 ); } } @@ -390,10 +372,40 @@ export class ShippingBanner extends Component { showShippingBanner, isShippingLabelButtonBusy, } = this.state; + const { isWcstCompatible } = this.props; + if ( ! showShippingBanner && ! isWcstCompatible ) { + document + .getElementById( 'woocommerce-admin-print-label' ) + .classList.add( 'error' ); + + return ( +

+ + { interpolateComponents( { + mixedString: __( + 'Please {{pluginPageLink}}update{{/pluginPageLink}} the WooCommerce Shipping & Tax plugin to the latest version to ensure compatibility with WooCommerce Shipping.', + 'woocommerce' + ), + components: { + pluginPageLink: ( + + ), + }, + } ) } + +

+ ); + } + if ( ! showShippingBanner ) { return null; } + const { actionButtonLabel, headline } = this.props; return (
@@ -403,15 +415,17 @@ export class ShippingBanner extends Component { alt={ __( 'Shipping ', 'woocommerce' ) } />
-

- { __( - 'Print discounted shipping labels with a click.', - 'woocommerce' - ) } -

+

{ headline }

{ interpolateComponents( { - mixedString: this.state.installText, + mixedString: sprintf( + // translators: %s is the action button label. + __( + 'By clicking "%s", {{wcsLink}}WooCommerce Shipping{{/wcsLink}} will be installed and you agree to its {{tosLink}}Terms of Service{{/tosLink}}.', + 'woocommerce' + ), + actionButtonLabel + ), components: { tosLink: ( - { __( 'Create shipping label', 'woocommerce' ) } + { actionButtonLabel }