From 057e118a3443721d947aa92c0487aac1238d288a Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 11 Sep 2024 13:47:02 +0800 Subject: [PATCH 1/4] Fix size for coming soon banner login button (#51251) * Fix coming soon banner login button style * Add changelog --- .../js/blocks/coming-soon/entire-site.scss | 20 +++++++++---------- .../changelog/fix-coming-soon-login-button | 5 +++++ 2 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-coming-soon-login-button diff --git a/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/entire-site.scss b/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/entire-site.scss index 7ad342e5079..6bd8ba151e8 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/entire-site.scss +++ b/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/entire-site.scss @@ -73,22 +73,20 @@ body:has(.woocommerce-coming-soon-banner) { } .wp-block-loginout { - background-color: #000; - border-radius: 6px; display: flex; - height: 40px; - width: 74px; - justify-content: center; - align-items: center; - gap: 10px; - box-sizing: border-box; a { + box-sizing: border-box; + background-color: #000; + border-radius: 6px; color: #fff; - text-decoration: none; - line-height: 17px; + gap: 10px; font-size: 14px; - font-weight: 500; + font-style: normal; + line-height: normal; + text-align: center; + text-decoration: none; + padding: 17px 16px; } } diff --git a/plugins/woocommerce/changelog/fix-coming-soon-login-button b/plugins/woocommerce/changelog/fix-coming-soon-login-button new file mode 100644 index 00000000000..44c9292e8a0 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-coming-soon-login-button @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Fix size for coming soon banner button + + From 007bd21a35a5745f9ea75dc9c6b224cce37cf168 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 11 Sep 2024 15:39:28 +0800 Subject: [PATCH 2/4] Clean up purchase task (#51274) * Remove Purchase task * Add changelog --- .../changelog/update-remove-purchase-task | 4 + .../OnboardingTasks/Tasks/Purchase.php | 203 ------------------ .../onboarding-tasks/tasks/purchase.php | 186 ---------------- 3 files changed, 4 insertions(+), 389 deletions(-) create mode 100644 plugins/woocommerce/changelog/update-remove-purchase-task delete mode 100644 plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Purchase.php delete mode 100644 plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/tasks/purchase.php diff --git a/plugins/woocommerce/changelog/update-remove-purchase-task b/plugins/woocommerce/changelog/update-remove-purchase-task new file mode 100644 index 00000000000..20806d693a2 --- /dev/null +++ b/plugins/woocommerce/changelog/update-remove-purchase-task @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Clean up Purchase task diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Purchase.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Purchase.php deleted file mode 100644 index 2c9383f4a60..00000000000 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Purchase.php +++ /dev/null @@ -1,203 +0,0 @@ -undo_dismiss(); - } - - /** - * Get the task arguments. - * ID. - * - * @return string - */ - public function get_id() { - return 'purchase'; - } - - /** - * Title. - * - * @return string - */ - public function get_title() { - $products = $this->get_paid_products_and_themes(); - $first_product = count( $products['purchaseable'] ) >= 1 ? $products['purchaseable'][0] : false; - - if ( ! $first_product ) { - return null; - } - - $product_label = isset( $first_product['label'] ) ? $first_product['label'] : $first_product['title']; - $additional_count = count( $products['purchaseable'] ) - 1; - - if ( $this->get_parent_option( 'use_completed_title' ) && $this->is_complete() ) { - return count( $products['purchaseable'] ) === 1 - ? sprintf( - /* translators: %1$s: a purchased product name */ - __( - 'You added %1$s', - 'woocommerce' - ), - $product_label - ) - : sprintf( - /* translators: %1$s: a purchased product name, %2$d the number of other products purchased */ - _n( - 'You added %1$s and %2$d other product', - 'You added %1$s and %2$d other products', - $additional_count, - 'woocommerce' - ), - $product_label, - $additional_count - ); - } - - return count( $products['purchaseable'] ) === 1 - ? sprintf( - /* translators: %1$s: a purchaseable product name */ - __( - 'Add %s to my store', - 'woocommerce' - ), - $product_label - ) - : sprintf( - /* translators: %1$s: a purchaseable product name, %2$d the number of other products to purchase */ - _n( - 'Add %1$s and %2$d more product to my store', - 'Add %1$s and %2$d more products to my store', - $additional_count, - 'woocommerce' - ), - $product_label, - $additional_count - ); - } - - /** - * Content. - * - * @return string - */ - public function get_content() { - $products = $this->get_paid_products_and_themes(); - - if ( count( $products['remaining'] ) === 1 ) { - return isset( $products['purchaseable'][0]['description'] ) ? $products['purchaseable'][0]['description'] : $products['purchaseable'][0]['excerpt']; - } - return sprintf( - /* translators: %1$s: list of product names comma separated, %2%s the last product name */ - __( - 'Good choice! You chose to add %1$s and %2$s to your store.', - 'woocommerce' - ), - implode( ', ', array_slice( $products['remaining'], 0, -1 ) ) . ( count( $products['remaining'] ) > 2 ? ',' : '' ), - end( $products['remaining'] ) - ); - } - - /** - * Action label. - * - * @return string - */ - public function get_action_label() { - return __( 'Purchase & install now', 'woocommerce' ); - } - - - /** - * Time. - * - * @return string - */ - public function get_time() { - return __( '2 minutes', 'woocommerce' ); - } - - /** - * Task completion. - * - * @return bool - */ - public function is_complete() { - $products = $this->get_paid_products_and_themes(); - return count( $products['remaining'] ) === 0; - } - - /** - * Dismissable. - * - * @return bool - */ - public function is_dismissable() { - return true; - } - - /** - * Task visibility. - * - * @return bool - */ - public function can_view() { - $products = $this->get_paid_products_and_themes(); - return count( $products['purchaseable'] ) > 0; - } - - /** - * Get purchaseable and remaining products. - * - * @return array purchaseable and remaining products and themes. - */ - public static function get_paid_products_and_themes() { - $relevant_products = OnboardingProducts::get_relevant_products(); - - $profiler_data = get_option( OnboardingProfile::DATA_OPTION, array() ); - $theme = isset( $profiler_data['theme'] ) ? $profiler_data['theme'] : null; - $paid_theme = $theme ? OnboardingThemes::get_paid_theme_by_slug( $theme ) : null; - if ( $paid_theme ) { - - $relevant_products['purchaseable'][] = $paid_theme; - - if ( isset( $paid_theme['is_installed'] ) && false === $paid_theme['is_installed'] ) { - $relevant_products['remaining'][] = $paid_theme['title']; - } - } - return $relevant_products; - } -} diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/tasks/purchase.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/tasks/purchase.php deleted file mode 100644 index 043052a0371..00000000000 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/tasks/purchase.php +++ /dev/null @@ -1,186 +0,0 @@ -task = new Purchase( new TaskList() ); - set_transient( - OnboardingThemes::THEMES_TRANSIENT, - array( - 'free' => array( - 'slug' => 'free', - 'is_installed' => false, - ), - 'paid' => array( - 'slug' => 'paid', - 'id' => 12312, - 'price' => '$79.00', - 'title' => 'theme title', - 'is_installed' => false, - ), - 'paid_installed' => array( - 'slug' => 'paid_installed', - 'id' => 12312, - 'price' => '$79.00', - 'title' => 'theme title', - 'is_installed' => true, - ), - 'free_with_price' => array( - 'slug' => 'free_with_price', - 'id' => 12312, - 'price' => '$0.00', - 'title' => 'theme title', - 'is_installed' => false, - ), - ) - ); - } - - /** - * Tear down. - */ - public function tearDown(): void { - parent::tearDown(); - delete_transient( OnboardingThemes::THEMES_TRANSIENT ); - delete_option( OnboardingProfile::DATA_OPTION ); - } - - /** - * Test is_complete function of Purchase task. - */ - public function test_is_complete_if_no_remaining_products() { - update_option( OnboardingProfile::DATA_OPTION, array( 'product_types' => array( 'physical' ) ) ); - $this->assertEquals( true, $this->task->is_complete() ); - } - - /** - * Test is_complete function of Purchase task. - */ - public function test_is_not_complete_if_remaining_paid_products() { - update_option( OnboardingProfile::DATA_OPTION, array( 'product_types' => array( 'memberships' ) ) ); - $this->assertEquals( false, $this->task->is_complete() ); - } - - /** - * Test is_complete function of Purchase task. - */ - public function test_is_complete_if_no_paid_themes() { - update_option( - OnboardingProfile::DATA_OPTION, - array( - 'product_types' => array(), - 'theme' => 'free', - ) - ); - $this->assertEquals( true, $this->task->is_complete() ); - } - - /** - * Test is_complete function of Purchase task. - */ - public function test_is_not_complete_if_paid_theme_that_is_not_installed() { - update_option( - OnboardingProfile::DATA_OPTION, - array( - 'product_types' => array(), - 'theme' => 'paid', - ) - ); - $this->assertEquals( false, $this->task->is_complete() ); - } - - /** - * Test is_complete function of Purchase task. - */ - public function test_is_complete_if_paid_theme_that_is_installed() { - update_option( - OnboardingProfile::DATA_OPTION, - array( - 'product_types' => array(), - 'theme' => 'paid_installed', - ) - ); - $this->assertEquals( true, $this->task->is_complete() ); - } - - /** - * Test is_complete function of Purchase task. - */ - public function test_is_complete_if_free_theme_with_set_price() { - update_option( - OnboardingProfile::DATA_OPTION, - array( - 'product_types' => array(), - 'theme' => 'free_with_price', - ) - ); - $this->assertEquals( true, $this->task->is_complete() ); - } - - /** - * Test the task title for a single paid item. - */ - public function test_get_title_if_single_paid_item() { - update_option( - OnboardingProfile::DATA_OPTION, - array( - 'product_types' => array(), - 'theme' => 'paid', - ) - ); - $this->assertEquals( 'Add theme title to my store', $this->task->get_title() ); - } - - /** - * Test the task title if 2 paid items exist. - */ - public function test_get_title_if_multiple_paid_themes() { - update_option( - OnboardingProfile::DATA_OPTION, - array( - 'product_types' => array( 'memberships' ), - 'theme' => 'paid', - ) - ); - $this->assertEquals( 'Add Memberships and 1 more product to my store', $this->task->get_title() ); - } - - /** - * Test the task title if multiple additional paid items exist. - */ - public function test_get_title_if_multiple_paid_products() { - update_option( - OnboardingProfile::DATA_OPTION, - array( - 'product_types' => array( 'memberships', 'bookings' ), - 'theme' => 'paid', - ) - ); - $this->assertEquals( 'Add Memberships and 2 more products to my store', $this->task->get_title() ); - } -} From b728f53f2902d56214d05ac34ac0266fafde85ad Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 11 Sep 2024 16:13:48 +0800 Subject: [PATCH 3/4] Fix duplicate spec evaluation in `evaluate_specs()` (#51166) * feat: Add memoization to EvaluateSuggestion class This commit adds memoization to the `EvaluateSuggestion` class in order to improve performance by caching the results of the `evaluate_specs` method. The memoization is implemented using an associative array called `$memo`, which stores the results of previous evaluations based on the input specifications and logger arguments. The memoization logic checks if the results for a given set of specifications and logger arguments already exist in the `$memo` array, and if so, returns the cached results instead of re-evaluating the specs. This helps to avoid redundant computations and improves the overall efficiency of the `evaluate_specs` method. * Add changelog * Only use specs as key * Move shipping test to correct folder * Fix tests * Fix tests * Fix lint * Address PR feedback --- .../changelog/fix-duplicate-spec-evaluation | 4 ++ .../EvaluateSuggestion.php | 49 ++++++++++++++++++- .../evaluate-suggestion.php | 41 ++++++++++++++++ .../payment-gateway-suggestions.php | 9 ++-- .../DefaultShippingPartnersTest.php | 4 +- .../ShippingPartnerSuggestionsTest.php | 7 ++- .../WCPayPromotion/DefaultPromotionsTest.php | 2 + .../Admin/WCPayPromotion/InitTest.php | 6 ++- 8 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-duplicate-spec-evaluation rename plugins/woocommerce/tests/php/src/Admin/{ => Features}/ShippingPartnerSuggestions/DefaultShippingPartnersTest.php (97%) diff --git a/plugins/woocommerce/changelog/fix-duplicate-spec-evaluation b/plugins/woocommerce/changelog/fix-duplicate-spec-evaluation new file mode 100644 index 00000000000..299eaf7c9d6 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-duplicate-spec-evaluation @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix duplicate spec evaluation in evaluate_specs() diff --git a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php index 45d609da940..4b614367199 100644 --- a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php +++ b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php @@ -13,6 +13,13 @@ use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\RuleEvaluator; * Evaluates the spec and returns the evaluated suggestion. */ class EvaluateSuggestion { + /** + * Stores memoized results of evaluate_specs. + * + * @var array + */ + protected static $memo = array(); + /** * Evaluates the spec and returns the suggestion. * @@ -58,6 +65,12 @@ class EvaluateSuggestion { * @return array The visible suggestions and errors. */ public static function evaluate_specs( $specs, $logger_args = array() ) { + $specs_key = self::get_memo_key( $specs ); + + if ( isset( self::$memo[ $specs_key ] ) ) { + return self::$memo[ $specs_key ]; + } + $suggestions = array(); $errors = array(); @@ -72,9 +85,43 @@ class EvaluateSuggestion { } } - return array( + $result = array( 'suggestions' => $suggestions, 'errors' => $errors, ); + + // Memoize results, with a fail safe to prevent unbounded memory growth. + // This limit is unlikely to be reached under normal circumstances. + if ( count( self::$memo ) > 50 ) { + self::reset_memo(); + } + self::$memo[ $specs_key ] = $result; + + return $result; + } + + /** + * Resets the memoized results. Useful for testing. + */ + public static function reset_memo() { + self::$memo = array(); + } + + /** + * Returns a memoization key for the given specs. + * + * @param array $specs The specs to generate a key for. + * + * @return string The memoization key. + */ + private static function get_memo_key( $specs ) { + $data = wp_json_encode( $specs ); + + if ( function_exists( 'hash' ) && in_array( 'xxh3', hash_algos(), true ) ) { + // Use xxHash (xxh3) if available. + return hash( 'xxh3', $data ); + } + // Fall back to CRC32. + return (string) crc32( $data ); } } diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/evaluate-suggestion.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/evaluate-suggestion.php index 15fee01caea..3b0a70952c6 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/evaluate-suggestion.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/evaluate-suggestion.php @@ -323,6 +323,31 @@ class WC_Admin_Tests_PaymentGatewaySuggestions_EvaluateSuggestion extends WC_Uni remove_filter( 'woocommerce_admin_remote_specs_evaluator_should_log', '__return_true' ); } + /** + * Test that the memo is set correctly. + */ + public function test_memo_set_correctly() { + $specs = array( + array( + 'id' => 'test-gateway-1', + 'is_visible' => true, + ), + array( + 'id' => 'test-gateway-2', + 'is_visible' => false, + ), + ); + + $result = TestableEvaluateSuggestion::evaluate_specs( $specs ); + $memo = TestableEvaluateSuggestion::get_memo_for_tests(); + + $this->assertCount( 1, $memo ); + $memo_key = array_keys( $memo )[0]; + $this->assertEquals( $result, $memo[ $memo_key ] ); + $this->assertCount( 1, $result['suggestions'] ); + $this->assertEquals( 'test-gateway-1', $result['suggestions'][0]->id ); + } + /** * Overrides the WC logger. * @@ -359,3 +384,19 @@ class WC_Admin_Tests_PaymentGatewaySuggestions_EvaluateSuggestion extends WC_Uni ); } } + +//phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound, Squiz.Classes.ClassFileName.NoMatch, Suin.Classes.PSR4.IncorrectClassName +/** + * TestableEvaluateSuggestion class. + */ +class TestableEvaluateSuggestion extends EvaluateSuggestion { + /** + * Get the memo for testing. + * + * @return array + */ + public static function get_memo_for_tests() { + return self::$memo; + } +} +//phpcs:enable Generic.Files.OneObjectStructurePerFile.MultipleFound, Squiz.Classes.ClassFileName.NoMatch, Suin.Classes.PSR4.IncorrectClassName diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/payment-gateway-suggestions.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/payment-gateway-suggestions.php index 728c78ce286..bb2ef0f93f1 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/payment-gateway-suggestions.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/payment-gateway-suggestions.php @@ -25,7 +25,7 @@ class WC_Admin_Tests_PaymentGatewaySuggestions_Init extends WC_Unit_Test_Case { delete_option( 'woocommerce_show_marketplace_suggestions' ); add_filter( 'transient_woocommerce_admin_' . PaymentGatewaySuggestionsDataSourcePoller::ID . '_specs', - function( $value ) { + function ( $value ) { if ( $value ) { return $value; } @@ -37,6 +37,8 @@ class WC_Admin_Tests_PaymentGatewaySuggestions_Init extends WC_Unit_Test_Case { ); } ); + + EvaluateSuggestion::reset_memo(); } /** @@ -57,7 +59,7 @@ class WC_Admin_Tests_PaymentGatewaySuggestions_Init extends WC_Unit_Test_Case { remove_all_filters( 'transient_woocommerce_admin_' . PaymentGatewaySuggestionsDataSourcePoller::ID . '_specs' ); add_filter( DataSourcePoller::FILTER_NAME, - function() { + function () { return array(); } ); @@ -242,7 +244,7 @@ class WC_Admin_Tests_PaymentGatewaySuggestions_Init extends WC_Unit_Test_Case { add_filter( 'locale', - function( $_locale ) { + function () { return 'zh_TW'; } ); @@ -364,5 +366,4 @@ class WC_Admin_Tests_PaymentGatewaySuggestions_Init extends WC_Unit_Test_Case { // Clean up. delete_option( PaymentGatewaySuggestions::RECOMMENDED_PAYMENT_PLUGINS_DISMISS_OPTION ); } - } diff --git a/plugins/woocommerce/tests/php/src/Admin/ShippingPartnerSuggestions/DefaultShippingPartnersTest.php b/plugins/woocommerce/tests/php/src/Admin/Features/ShippingPartnerSuggestions/DefaultShippingPartnersTest.php similarity index 97% rename from plugins/woocommerce/tests/php/src/Admin/ShippingPartnerSuggestions/DefaultShippingPartnersTest.php rename to plugins/woocommerce/tests/php/src/Admin/Features/ShippingPartnerSuggestions/DefaultShippingPartnersTest.php index ecea313caa7..44b15c965f6 100644 --- a/plugins/woocommerce/tests/php/src/Admin/ShippingPartnerSuggestions/DefaultShippingPartnersTest.php +++ b/plugins/woocommerce/tests/php/src/Admin/Features/ShippingPartnerSuggestions/DefaultShippingPartnersTest.php @@ -1,7 +1,7 @@ mock_logger = $this->getMockBuilder( 'WC_Logger_Interface' )->getMock(); add_filter( 'woocommerce_logging_class', array( $this, 'override_wc_logger' ) ); + + EvaluateSuggestion::reset_memo(); } /** @@ -91,7 +94,7 @@ class ShippingPartnerSuggestionsTest extends WC_Unit_Test_Case { remove_all_filters( 'transient_woocommerce_admin_' . ShippingPartnerSuggestionsDataSourcePoller::ID . '_specs' ); add_filter( DataSourcePoller::FILTER_NAME, - function() { + function () { return array(); } ); diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/WCPayPromotion/DefaultPromotionsTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/WCPayPromotion/DefaultPromotionsTest.php index 893cc7a9753..29351fce779 100644 --- a/plugins/woocommerce/tests/php/src/Internal/Admin/WCPayPromotion/DefaultPromotionsTest.php +++ b/plugins/woocommerce/tests/php/src/Internal/Admin/WCPayPromotion/DefaultPromotionsTest.php @@ -29,6 +29,8 @@ class DefaultPromotionsTest extends WC_Unit_Test_Case { update_option( 'woocommerce_store_address', 'foo' ); update_option( 'active_plugins', array( 'foo/foo.php' ) ); + + EvaluateSuggestion::reset_memo(); } /** diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/WCPayPromotion/InitTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/WCPayPromotion/InitTest.php index 3a51a919fa3..cfe7b606f69 100644 --- a/plugins/woocommerce/tests/php/src/Internal/Admin/WCPayPromotion/InitTest.php +++ b/plugins/woocommerce/tests/php/src/Internal/Admin/WCPayPromotion/InitTest.php @@ -26,7 +26,7 @@ class InitTest extends WC_Unit_Test_Case { delete_option( 'woocommerce_show_marketplace_suggestions' ); add_filter( 'transient_woocommerce_admin_' . WCPayPromotionDataSourcePoller::ID . '_specs', - function( $value ) { + function ( $value ) { if ( $value ) { return $value; } @@ -38,6 +38,8 @@ class InitTest extends WC_Unit_Test_Case { ); } ); + + EvaluateSuggestion::reset_memo(); } /** @@ -59,7 +61,7 @@ class InitTest extends WC_Unit_Test_Case { remove_all_filters( 'transient_woocommerce_admin_' . WCPayPromotionDataSourcePoller::ID . '_specs' ); add_filter( DataSourcePoller::FILTER_NAME, - function() { + function () { return array(); } ); From ab0e76c943cdd1323b32a81a753dfae88a22c0fc Mon Sep 17 00:00:00 2001 From: Boro Sitnikovski Date: Wed, 11 Sep 2024 13:00:49 +0200 Subject: [PATCH 4/4] Add search results count to the in-app marketplace (#51266) * Add searchResults to context * Use setSearchResults in Content * Add ribbons to the tabs * Changelog * Use setState as the function name * Only show ribbon counts when there's an active search * Refactor how 'setSearchResultsCount' is used (h/t @mcliwanow) * Don't populate initial search results * Unify css styling --- .../components/content/content.tsx | 81 +++++++++---- .../marketplace/components/tabs/tabs.scss | 4 + .../marketplace/components/tabs/tabs.tsx | 113 ++++++++++-------- .../contexts/marketplace-context.tsx | 33 ++++- .../client/marketplace/contexts/types.ts | 10 ++ .../tweak-21597-in-app-search-results-count | 4 + 6 files changed, 169 insertions(+), 76 deletions(-) create mode 100644 plugins/woocommerce/changelog/tweak-21597-in-app-search-results-count diff --git a/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx b/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx index faf30ef8134..7971dde66a0 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx @@ -17,6 +17,8 @@ import MySubscriptions from '../my-subscriptions/my-subscriptions'; import { MarketplaceContext } from '../../contexts/marketplace-context'; import { fetchSearchResults } from '../../utils/functions'; import { SubscriptionsContextProvider } from '../../contexts/subscriptions-context'; +import { SearchResultsCountType } from '../../contexts/types'; + import { recordMarketplaceView, recordLegacyTabView, @@ -30,40 +32,51 @@ import SubscriptionsExpiredExpiringNotice from '~/marketplace/components/my-subs export default function Content(): JSX.Element { const marketplaceContextValue = useContext( MarketplaceContext ); const [ products, setProducts ] = useState< Product[] >( [] ); - const { setIsLoading, selectedTab, setHasBusinessServices } = - marketplaceContextValue; + const { + setIsLoading, + selectedTab, + setHasBusinessServices, + setSearchResultsCount, + } = marketplaceContextValue; const query = useQuery(); // On initial load of the in-app marketplace, fetch extensions, themes and business services // and check if there are any business services available on WCCOM useEffect( () => { - const categories = [ '', 'themes', 'business-services' ]; + const categories: Array< keyof SearchResultsCountType > = [ + 'extensions', + 'themes', + 'business-services', + ]; const abortControllers = categories.map( () => new AbortController() ); - categories.forEach( ( category: string, index ) => { - const params = new URLSearchParams(); - if ( category !== '' ) { - params.append( 'category', category ); - } + categories.forEach( + ( category: keyof SearchResultsCountType, index ) => { + const params = new URLSearchParams(); + if ( category !== 'extensions' ) { + params.append( 'category', category ); + } - const wccomSettings = getAdminSetting( 'wccomHelper', false ); - if ( wccomSettings.storeCountry ) { - params.append( 'country', wccomSettings.storeCountry ); - } + const wccomSettings = getAdminSetting( 'wccomHelper', false ); + if ( wccomSettings.storeCountry ) { + params.append( 'country', wccomSettings.storeCountry ); + } - fetchSearchResults( params, abortControllers[ index ].signal ).then( - ( productList ) => { + fetchSearchResults( + params, + abortControllers[ index ].signal + ).then( ( productList ) => { if ( category === 'business-services' ) { setHasBusinessServices( productList.length > 0 ); } - } - ); - return () => { - abortControllers.forEach( ( controller ) => { - controller.abort(); } ); - }; - } ); + return () => { + abortControllers.forEach( ( controller ) => { + controller.abort(); + } ); + }; + } + ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [] ); @@ -108,6 +121,20 @@ export default function Content(): JSX.Element { fetchSearchResults( params, abortController.signal ) .then( ( productList ) => { setProducts( productList ); + + if ( query.term ) { + setSearchResultsCount( { + extensions: productList.filter( + ( p ) => p.type === 'extension' + ).length, + themes: productList.filter( + ( p ) => p.type === 'theme' + ).length, + 'business-services': productList.filter( + ( p ) => p.type === 'business-service' + ).length, + } ); + } } ) .catch( () => { setProducts( [] ); @@ -142,7 +169,9 @@ export default function Content(): JSX.Element { case 'extensions': return ( p.type === 'extension' + ) } categorySelector={ true } type={ ProductType.extension } /> @@ -150,7 +179,9 @@ export default function Content(): JSX.Element { case 'themes': return ( p.type === 'theme' + ) } categorySelector={ true } type={ ProductType.theme } /> @@ -158,7 +189,9 @@ export default function Content(): JSX.Element { case 'business-services': return ( p.type === 'business-service' + ) } categorySelector={ true } type={ ProductType.businessService } /> diff --git a/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss b/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss index 008fe4e1b84..354a1797917 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss +++ b/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss @@ -45,6 +45,10 @@ text-align: center; z-index: 26; } + + &__update-count { + background-color: #000; + } } @media (width <= $breakpoint-medium) { diff --git a/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.tsx b/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.tsx index 1f852a2b982..fd6fc62809f 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.tsx @@ -35,45 +35,6 @@ interface Tabs { const wccomSettings = getAdminSetting( 'wccomHelper', {} ); const wooUpdateCount = wccomSettings?.wooUpdateCount ?? 0; -const tabs: Tabs = { - search: { - name: 'search', - title: __( 'Search results', 'woocommerce' ), - showUpdateCount: false, - updateCount: 0, - }, - discover: { - name: 'discover', - title: __( 'Discover', 'woocommerce' ), - showUpdateCount: false, - updateCount: 0, - }, - extensions: { - name: 'extensions', - title: __( 'Extensions', 'woocommerce' ), - showUpdateCount: false, - updateCount: 0, - }, - themes: { - name: 'themes', - title: __( 'Themes', 'woocommerce' ), - showUpdateCount: false, - updateCount: 0, - }, - 'business-services': { - name: 'business-services', - title: __( 'Business services', 'woocommerce' ), - showUpdateCount: false, - updateCount: 0, - }, - 'my-subscriptions': { - name: 'my-subscriptions', - title: __( 'My subscriptions', 'woocommerce' ), - showUpdateCount: true, - updateCount: wooUpdateCount, - }, -}; - const setUrlTabParam = ( tabKey: string ) => { navigateTo( { url: getNewPath( @@ -84,7 +45,11 @@ const setUrlTabParam = ( tabKey: string ) => { } ); }; -const getVisibleTabs = ( selectedTab: string, hasBusinessServices = false ) => { +const getVisibleTabs = ( + selectedTab: string, + hasBusinessServices = false, + tabs: Tabs +) => { if ( selectedTab === '' ) { return tabs; } @@ -101,7 +66,8 @@ const getVisibleTabs = ( selectedTab: string, hasBusinessServices = false ) => { const renderTabs = ( marketplaceContextValue: MarketplaceContextType, - visibleTabs: Tabs + visibleTabs: Tabs, + tabs: Tabs ) => { const { selectedTab, setSelectedTab } = marketplaceContextValue; @@ -141,12 +107,13 @@ const renderTabs = ( key={ tabKey } > { tabs[ tabKey ]?.title } - { tabs[ tabKey ]?.showUpdateCount && - tabs[ tabKey ]?.updateCount > 0 && ( - - { tabs[ tabKey ]?.updateCount } - - ) } + { tabs[ tabKey ]?.showUpdateCount && ( + + { tabs[ tabKey ]?.updateCount } + + ) } ) ); @@ -159,10 +126,53 @@ const Tabs = ( props: TabsProps ): JSX.Element => { const marketplaceContextValue = useContext( MarketplaceContext ); const { selectedTab, setSelectedTab, hasBusinessServices } = marketplaceContextValue; - const [ visibleTabs, setVisibleTabs ] = useState( getVisibleTabs( '' ) ); + const { searchResultsCount } = marketplaceContextValue; const query: Record< string, string > = useQuery(); + const tabs: Tabs = { + search: { + name: 'search', + title: __( 'Search results', 'woocommerce' ), + showUpdateCount: false, + updateCount: 0, + }, + discover: { + name: 'discover', + title: __( 'Discover', 'woocommerce' ), + showUpdateCount: false, + updateCount: 0, + }, + extensions: { + name: 'extensions', + title: __( 'Extensions', 'woocommerce' ), + showUpdateCount: !! query.term, + updateCount: searchResultsCount.extensions, + }, + themes: { + name: 'themes', + title: __( 'Themes', 'woocommerce' ), + showUpdateCount: !! query.term, + updateCount: searchResultsCount.themes, + }, + 'business-services': { + name: 'business-services', + title: __( 'Business services', 'woocommerce' ), + showUpdateCount: !! query.term, + updateCount: searchResultsCount[ 'business-services' ], + }, + 'my-subscriptions': { + name: 'my-subscriptions', + title: __( 'My subscriptions', 'woocommerce' ), + showUpdateCount: true, + updateCount: wooUpdateCount, + }, + }; + + const [ visibleTabs, setVisibleTabs ] = useState( + getVisibleTabs( '', false, tabs ) + ); + useEffect( () => { if ( query?.tab && tabs[ query.tab ] ) { setSelectedTab( query.tab ); @@ -172,8 +182,11 @@ const Tabs = ( props: TabsProps ): JSX.Element => { }, [ query, setSelectedTab ] ); useEffect( () => { - setVisibleTabs( getVisibleTabs( selectedTab, hasBusinessServices ) ); + setVisibleTabs( + getVisibleTabs( selectedTab, hasBusinessServices, tabs ) + ); }, [ selectedTab, hasBusinessServices ] ); + return ( ); }; diff --git a/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx b/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx index 53dd1bb5789..0631bf5725e 100644 --- a/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx +++ b/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx @@ -1,12 +1,17 @@ /** * External dependencies */ -import { useState, useEffect, createContext } from '@wordpress/element'; +import { + useState, + useEffect, + useCallback, + createContext, +} from '@wordpress/element'; /** * Internal dependencies */ -import { MarketplaceContextType } from './types'; +import { SearchResultsCountType, MarketplaceContextType } from './types'; import { getAdminSetting } from '../../utils/admin-settings'; export const MarketplaceContext = createContext< MarketplaceContextType >( { @@ -18,6 +23,12 @@ export const MarketplaceContext = createContext< MarketplaceContextType >( { addInstalledProduct: () => {}, hasBusinessServices: false, setHasBusinessServices: () => {}, + searchResultsCount: { + extensions: 0, + themes: 0, + 'business-services': 0, + }, + setSearchResultsCount: () => {}, } ); export function MarketplaceContextProvider( props: { @@ -29,6 +40,22 @@ export function MarketplaceContextProvider( props: { [] ); const [ hasBusinessServices, setHasBusinessServices ] = useState( false ); + const [ searchResultsCount, setSearchResultsCountState ] = + useState< SearchResultsCountType >( { + extensions: 0, + themes: 0, + 'business-services': 0, + } ); + + const setSearchResultsCount = useCallback( + ( updatedCounts: Partial< SearchResultsCountType > ) => { + setSearchResultsCountState( ( prev ) => ( { + ...prev, + ...updatedCounts, + } ) ); + }, + [] + ); /** * Knowing installed products will help us to determine which products @@ -59,6 +86,8 @@ export function MarketplaceContextProvider( props: { addInstalledProduct, hasBusinessServices, setHasBusinessServices, + searchResultsCount, + setSearchResultsCount, }; return ( diff --git a/plugins/woocommerce-admin/client/marketplace/contexts/types.ts b/plugins/woocommerce-admin/client/marketplace/contexts/types.ts index 969ce88d2e1..3240729871e 100644 --- a/plugins/woocommerce-admin/client/marketplace/contexts/types.ts +++ b/plugins/woocommerce-admin/client/marketplace/contexts/types.ts @@ -8,6 +8,12 @@ import { Options } from '@wordpress/notices'; */ import { Subscription } from '../components/my-subscriptions/types'; +export interface SearchResultsCountType { + extensions: number; + themes: number; + 'business-services': number; +} + export type MarketplaceContextType = { isLoading: boolean; setIsLoading: ( isLoading: boolean ) => void; @@ -17,6 +23,10 @@ export type MarketplaceContextType = { addInstalledProduct: ( slug: string ) => void; hasBusinessServices: boolean; setHasBusinessServices: ( hasBusinessServices: boolean ) => void; + searchResultsCount: SearchResultsCountType; + setSearchResultsCount: ( + updatedCounts: Partial< SearchResultsCountType > + ) => void; }; export type SubscriptionsContextType = { diff --git a/plugins/woocommerce/changelog/tweak-21597-in-app-search-results-count b/plugins/woocommerce/changelog/tweak-21597-in-app-search-results-count new file mode 100644 index 00000000000..b0edda219c3 --- /dev/null +++ b/plugins/woocommerce/changelog/tweak-21597-in-app-search-results-count @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add search result counts to the in-app marketplace header tabs (Extensions area)