diff --git a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss
index d713efc1a80..952780f05e2 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss
+++ b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss
@@ -1,13 +1,17 @@
@import "../../stylesheets/_variables.scss";
.woocommerce-marketplace__category-selector {
+ position: relative;
display: flex;
align-items: stretch;
- margin: $grid-unit-20 0 0 0;
+ margin: 0;
+ overflow-x: auto;
}
.woocommerce-marketplace__category-item {
cursor: pointer;
+ white-space: nowrap;
+ margin-bottom: 0;
.components-dropdown {
height: 100%;
@@ -50,7 +54,6 @@
.woocommerce-marketplace__category-selector--full-width {
display: none;
- margin-top: $grid-unit-15;
}
@media screen and (max-width: $break-medium) {
@@ -122,3 +125,22 @@
background-color: $gray-900;
}
}
+
+.woocommerce-marketplace__category-navigation-button {
+ border: none;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ height: 100%;
+ width: 50px;
+}
+
+.woocommerce-marketplace__category-navigation-button--prev {
+ background: linear-gradient(to right, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
+ left: 0;
+}
+
+.woocommerce-marketplace__category-navigation-button--next {
+ background: linear-gradient(to left, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
+ right: 0;
+}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx
index 07b298cde66..9625aef5862 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx
@@ -1,20 +1,21 @@
/**
* External dependencies
*/
-import { useState, useEffect } from '@wordpress/element';
-import { __ } from '@wordpress/i18n';
+import { useState, useEffect, useRef } from '@wordpress/element';
import { useQuery } from '@woocommerce/navigation';
-import clsx from 'clsx';
+import { Icon } from '@wordpress/components';
+import { useDebounce } from '@wordpress/compose';
+import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import CategoryLink from './category-link';
-import CategoryDropdown from './category-dropdown';
import { Category, CategoryAPIItem } from './types';
import { fetchCategories } from '../../utils/functions';
-import './category-selector.scss';
import { ProductType } from '../product-list/types';
+import CategoryDropdown from './category-dropdown';
+import './category-selector.scss';
const ALL_CATEGORIES_SLUGS = {
[ ProductType.extension ]: '_all',
@@ -29,32 +30,21 @@ interface CategorySelectorProps {
export default function CategorySelector(
props: CategorySelectorProps
): JSX.Element {
- const [ visibleItems, setVisibleItems ] = useState< Category[] >( [] );
- const [ dropdownItems, setDropdownItems ] = useState< Category[] >( [] );
const [ selected, setSelected ] = useState< Category >();
const [ isLoading, setIsLoading ] = useState( false );
+ const [ categoriesToShow, setCategoriesToShow ] = useState< Category[] >(
+ []
+ );
+ const [ isOverflowing, setIsOverflowing ] = useState( false );
+ const [ scrollPosition, setScrollPosition ] = useState<
+ 'start' | 'middle' | 'end'
+ >( 'start' );
+
+ const categorySelectorRef = useRef< HTMLUListElement >( null );
+ const selectedCategoryRef = useRef< HTMLLIElement >( null );
const query = useQuery();
- useEffect( () => {
- // If no category is selected, show All as selected
- let categoryToSearch = ALL_CATEGORIES_SLUGS[ props.type ];
-
- if ( query.category ) {
- categoryToSearch = query.category;
- }
-
- const allCategories = visibleItems.concat( dropdownItems );
-
- const selectedCategory = allCategories.find(
- ( category ) => category.slug === categoryToSearch
- );
-
- if ( selectedCategory ) {
- setSelected( selectedCategory );
- }
- }, [ query.category, props.type, visibleItems, dropdownItems ] );
-
useEffect( () => {
setIsLoading( true );
@@ -72,21 +62,125 @@ export default function CategorySelector(
return category.slug !== '_featured';
} );
- // Split array into two from 7th item
- const visibleCategoryItems = categories.slice( 0, 7 );
- const dropdownCategoryItems = categories.slice( 7 );
-
- setVisibleItems( visibleCategoryItems );
- setDropdownItems( dropdownCategoryItems );
+ setCategoriesToShow( categories );
} )
.catch( () => {
- setVisibleItems( [] );
- setDropdownItems( [] );
+ setCategoriesToShow( [] );
} )
.finally( () => {
setIsLoading( false );
} );
- }, [ props.type ] );
+ }, [ props.type, setCategoriesToShow ] );
+
+ useEffect( () => {
+ // If no category is selected, show All as selected
+ let categoryToSearch = ALL_CATEGORIES_SLUGS[ props.type ];
+
+ if ( query.category ) {
+ categoryToSearch = query.category;
+ }
+
+ const selectedCategory = categoriesToShow.find(
+ ( category ) => category.slug === categoryToSearch
+ );
+
+ if ( selectedCategory ) {
+ setSelected( selectedCategory );
+ }
+ }, [ query.category, props.type, categoriesToShow ] );
+
+ useEffect( () => {
+ if ( selectedCategoryRef.current ) {
+ selectedCategoryRef.current.scrollIntoView( {
+ block: 'nearest',
+ inline: 'center',
+ } );
+ }
+ }, [ selected ] );
+
+ function checkOverflow() {
+ if (
+ categorySelectorRef.current &&
+ categorySelectorRef.current.parentElement?.scrollWidth
+ ) {
+ const isContentOverflowing =
+ categorySelectorRef.current.scrollWidth >
+ categorySelectorRef.current.parentElement.scrollWidth;
+
+ setIsOverflowing( isContentOverflowing );
+ }
+ }
+
+ function checkScrollPosition() {
+ const ulElement = categorySelectorRef.current;
+
+ if ( ! ulElement ) {
+ return;
+ }
+
+ const { scrollLeft, scrollWidth, clientWidth } = ulElement;
+
+ if ( scrollLeft < 10 ) {
+ setScrollPosition( 'start' );
+
+ return;
+ }
+
+ if ( scrollLeft + clientWidth < scrollWidth ) {
+ setScrollPosition( 'middle' );
+
+ return;
+ }
+
+ if ( scrollLeft + clientWidth === scrollWidth ) {
+ setScrollPosition( 'end' );
+ }
+ }
+
+ const debouncedCheckOverflow = useDebounce( checkOverflow, 300 );
+ const debouncedScrollPosition = useDebounce( checkScrollPosition, 100 );
+
+ function scrollCategories( scrollAmount: number ) {
+ if ( categorySelectorRef.current ) {
+ categorySelectorRef.current.scrollTo( {
+ left: categorySelectorRef.current.scrollLeft + scrollAmount,
+ behavior: 'smooth',
+ } );
+ }
+ }
+
+ function scrollToNextCategories() {
+ scrollCategories( 200 );
+ }
+
+ function scrollToPrevCategories() {
+ scrollCategories( -200 );
+ }
+
+ useEffect( () => {
+ window.addEventListener( 'resize', debouncedCheckOverflow );
+
+ const ulElement = categorySelectorRef.current;
+
+ if ( ulElement ) {
+ ulElement.addEventListener( 'scroll', debouncedScrollPosition );
+ }
+
+ return () => {
+ window.removeEventListener( 'resize', debouncedCheckOverflow );
+
+ if ( ulElement ) {
+ ulElement.removeEventListener(
+ 'scroll',
+ debouncedScrollPosition
+ );
+ }
+ };
+ }, [ debouncedCheckOverflow, debouncedScrollPosition ] );
+
+ useEffect( () => {
+ checkOverflow();
+ }, [ categoriesToShow ] );
function mobileCategoryDropdownLabel() {
const allCategoriesText = __( 'All Categories', 'woocommerce' );
@@ -102,16 +196,6 @@ export default function CategorySelector(
return selected.label;
}
- function isSelectedInDropdown() {
- if ( ! selected ) {
- return false;
- }
-
- return dropdownItems.find(
- ( category ) => category.slug === selected.slug
- );
- }
-
if ( isLoading ) {
return (
<>
@@ -131,50 +215,62 @@ export default function CategorySelector(
return (
<>
-
- { visibleItems.map( ( category ) => (
+
+ { categoriesToShow.map( ( category ) => (
-
) ) }
- -
- { dropdownItems.length > 0 && (
-
- ) }
-
-
+ { isOverflowing && (
+ <>
+
+
+ >
+ ) }
>
);
}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/constants.ts b/plugins/woocommerce-admin/client/marketplace/components/constants.ts
index de76eded4e2..d9b7febe49a 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/constants.ts
+++ b/plugins/woocommerce-admin/client/marketplace/components/constants.ts
@@ -10,7 +10,7 @@ export const MARKETPLACE_SEARCH_API_PATH =
'/wp-json/wccom-extensions/1.0/search';
export const MARKETPLACE_CATEGORY_API_PATH =
'/wp-json/wccom-extensions/1.0/categories';
-export const MARKETPLACE_ITEMS_PER_PAGE = 60;
+export const MARKETPLACE_ITEMS_PER_PAGE = 60; // This should match the number of results returned by the API
export const MARKETPLACE_SEARCH_RESULTS_PER_PAGE = 8;
export const MARKETPLACE_CART_PATH = MARKETPLACE_HOST + '/cart/';
export const MARKETPLACE_RENEW_SUBSCRIPTON_PATH =
diff --git a/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx b/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx
index 1a4679dc4c1..06f1a17825b 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx
@@ -1,22 +1,29 @@
/**
* External dependencies
*/
-import { useContext, useEffect, useState } from '@wordpress/element';
+import {
+ useContext,
+ useEffect,
+ useState,
+ useCallback,
+} from '@wordpress/element';
import { useQuery } from '@woocommerce/navigation';
+import { speak } from '@wordpress/a11y';
+import { __, sprintf } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import './content.scss';
-import { Product, ProductType, SearchResultType } from '../product-list/types';
+import { Product, ProductType } from '../product-list/types';
import { getAdminSetting } from '~/utils/admin-settings';
import Discover from '../discover/discover';
import Products from '../products/products';
-import SearchResults from '../search-results/search-results';
import MySubscriptions from '../my-subscriptions/my-subscriptions';
import { MarketplaceContext } from '../../contexts/marketplace-context';
-import { fetchSearchResults } from '../../utils/functions';
+import { fetchSearchResults, getProductType } from '../../utils/functions';
import { SubscriptionsContextProvider } from '../../contexts/subscriptions-context';
+import { SearchResultsCountType } from '../../contexts/types';
import {
recordMarketplaceView,
recordLegacyTabView,
@@ -26,149 +33,350 @@ import Promotions from '../promotions/promotions';
import ConnectNotice from '~/marketplace/components/connect-notice/connect-notice';
import PluginInstallNotice from '../woo-update-manager-plugin/plugin-install-notice';
import SubscriptionsExpiredExpiringNotice from '~/marketplace/components/my-subscriptions/subscriptions-expired-expiring-notice';
+import LoadMoreButton from '../load-more-button/load-more-button';
export default function Content(): JSX.Element {
const marketplaceContextValue = useContext( MarketplaceContext );
- const [ products, setProducts ] = useState< Product[] >( [] );
- const { setIsLoading, selectedTab, setHasBusinessServices } =
- marketplaceContextValue;
+ const [ allProducts, setAllProducts ] = useState< Product[] >( [] );
+ const [ filteredProducts, setFilteredProducts ] = useState< Product[] >(
+ []
+ );
+ const [ currentPage, setCurrentPage ] = useState( 1 );
+ const [ totalPagesCategory, setTotalPagesCategory ] = useState( 1 );
+ const [ totalPagesExtensions, setTotalPagesExtensions ] = useState( 1 );
+ const [ totalPagesThemes, setTotalPagesThemes ] = useState( 1 );
+ const [ totalPagesBusinessServices, setTotalPagesBusinessServices ] =
+ useState( 1 );
+ const [ firstNewProductId, setFirstNewProductId ] = useState< number >( 0 );
+ const [ isLoadingMore, setIsLoadingMore ] = useState( false );
+
+ const {
+ isLoading,
+ setIsLoading,
+ selectedTab,
+ setHasBusinessServices,
+ setSearchResultsCount,
+ } = marketplaceContextValue;
const query = useQuery();
- // On initial load of the in-app marketplace, fetch extensions, themes and business services
- // and check if there are any business services available on WCCOM
- useEffect( () => {
- const categories = [ '', 'themes', 'business-services' ];
- const abortControllers = categories.map( () => new AbortController() );
+ const searchCompleteAnnouncement = ( count: number ): void => {
+ speak(
+ sprintf(
+ // translators: %d is the number of products found.
+ __( '%d products found', 'woocommerce' ),
+ count
+ )
+ );
+ };
- categories.forEach( ( category: string, index ) => {
- const params = new URLSearchParams();
- if ( category !== '' ) {
- params.append( 'category', category );
- }
+ const tagProductsWithType = (
+ products: Product[],
+ type: ProductType
+ ): Product[] => {
+ return products.map( ( product ) => ( {
+ ...product,
+ type,
+ } ) );
+ };
- const wccomSettings = getAdminSetting( 'wccomHelper', false );
- if ( wccomSettings.storeCountry ) {
- params.append( 'country', wccomSettings.storeCountry );
- }
-
- fetchSearchResults( params, abortControllers[ index ].signal ).then(
- ( productList ) => {
- if ( category === 'business-services' ) {
- setHasBusinessServices( productList.length > 0 );
- }
- }
- );
- return () => {
- abortControllers.forEach( ( controller ) => {
- controller.abort();
- } );
- };
- } );
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [] );
-
- // Get the content for this screen
- useEffect( () => {
+ const loadMoreProducts = useCallback( () => {
+ setIsLoadingMore( true );
+ const params = new URLSearchParams();
const abortController = new AbortController();
- if (
- query.tab === undefined ||
- ( query.tab &&
- [ '', 'discover', 'my-subscriptions' ].includes( query.tab ) )
- ) {
- return;
+ if ( query.category && query.category !== '_all' ) {
+ params.append( 'category', query.category );
}
- setIsLoading( true );
- setProducts( [] );
-
- const params = new URLSearchParams();
+ if ( query.tab === 'themes' || query.tab === 'business-services' ) {
+ params.append( 'category', query.tab );
+ }
if ( query.term ) {
params.append( 'term', query.term );
}
- if ( query.category ) {
- params.append(
- 'category',
- query.category === '_all' ? '' : query.category
- );
- } else if ( query?.tab === 'themes' ) {
- params.append( 'category', 'themes' );
- } else if ( query?.tab === 'business-services' ) {
- params.append( 'category', 'business-services' );
- } else if ( query?.tab === 'search' ) {
- params.append( 'category', 'extensions-themes-business-services' );
- }
-
const wccomSettings = getAdminSetting( 'wccomHelper', false );
if ( wccomSettings.storeCountry ) {
params.append( 'country', wccomSettings.storeCountry );
}
+ params.append( 'page', ( currentPage + 1 ).toString() );
+
fetchSearchResults( params, abortController.signal )
.then( ( productList ) => {
- setProducts( productList );
+ setAllProducts( ( prevProducts ) => {
+ const flattenedPrevProducts = Array.isArray(
+ prevProducts[ 0 ]
+ )
+ ? prevProducts.flat()
+ : prevProducts;
+
+ const newProducts = productList.products.filter(
+ ( newProduct ) =>
+ ! flattenedPrevProducts.some(
+ ( prevProduct ) =>
+ prevProduct.id === newProduct.id
+ )
+ );
+
+ if ( newProducts.length > 0 ) {
+ setFirstNewProductId( newProducts[ 0 ].id ?? 0 );
+ }
+
+ const combinedProducts = [
+ ...flattenedPrevProducts,
+ ...newProducts,
+ ];
+
+ return combinedProducts;
+ } );
+
+ speak( __( 'More products loaded', 'woocommerce' ) );
+ setCurrentPage( ( prevPage ) => prevPage + 1 );
+ setIsLoadingMore( false );
} )
.catch( () => {
- setProducts( [] );
+ speak( __( 'Error loading more products', 'woocommerce' ) );
} )
.finally( () => {
- // we are recording both the new and legacy events here for now
- // they're separate methods to make it easier to remove the legacy one later
- const marketplaceViewProps = {
- view: query?.tab,
- search_term: query?.term,
- product_type: query?.section,
- category: query?.category,
- };
-
- recordMarketplaceView( marketplaceViewProps );
- recordLegacyTabView( marketplaceViewProps );
- setIsLoading( false );
+ setIsLoadingMore( false );
} );
+
return () => {
abortController.abort();
};
}, [
+ currentPage,
+ query.category,
+ query.term,
+ query.tab,
+ setIsLoadingMore,
+ ] );
+
+ useEffect( () => {
+ // if it's a paginated request, don't use this effect
+ if ( currentPage > 1 ) {
+ return;
+ }
+
+ const categories: Array< {
+ category: keyof SearchResultsCountType;
+ type: ProductType;
+ } > = [
+ { category: 'extensions', type: ProductType.extension },
+ { category: 'themes', type: ProductType.theme },
+ {
+ category: 'business-services',
+ type: ProductType.businessService,
+ },
+ ];
+ const abortControllers = categories.map( () => new AbortController() );
+
+ setIsLoading( true );
+ setAllProducts( [] );
+
+ // If query.category is present and not '_all', only fetch that category
+ if ( query.category && query.category !== '_all' ) {
+ const params = new URLSearchParams();
+
+ params.append( 'category', query.category );
+
+ if ( query.term ) {
+ params.append( 'term', query.term );
+ }
+
+ const wccomSettings = getAdminSetting( 'wccomHelper', false );
+ if ( wccomSettings.storeCountry ) {
+ params.append( 'country', wccomSettings.storeCountry );
+ }
+
+ fetchSearchResults( params, abortControllers[ 0 ].signal )
+ .then( ( productList ) => {
+ setAllProducts( productList.products );
+ setTotalPagesCategory( productList.totalPages );
+ setSearchResultsCount( {
+ [ query.tab ]: productList.totalProducts,
+ } );
+
+ searchCompleteAnnouncement( productList.totalProducts );
+ } )
+ .catch( () => {
+ setAllProducts( [] );
+ } )
+ .finally( () => {
+ setIsLoading( false );
+ } );
+ } else {
+ // Fetch all tabs when query.term or query.category changes
+ Promise.all(
+ categories.map( ( { category, type }, index ) => {
+ const params = new URLSearchParams();
+ if ( category !== 'extensions' ) {
+ params.append( 'category', category );
+ }
+ if ( query.term ) {
+ params.append( 'term', query.term );
+ }
+
+ const wccomSettings = getAdminSetting(
+ 'wccomHelper',
+ false
+ );
+ if ( wccomSettings.storeCountry ) {
+ params.append( 'country', wccomSettings.storeCountry );
+ }
+
+ return fetchSearchResults(
+ params,
+ abortControllers[ index ].signal
+ ).then( ( productList ) => {
+ const typedProducts = tagProductsWithType(
+ productList.products,
+ type
+ );
+ if ( category === 'business-services' ) {
+ setHasBusinessServices( typedProducts.length > 0 );
+ }
+ return {
+ products: typedProducts,
+ totalPages: productList.totalPages,
+ totalProducts: productList.totalProducts,
+ type,
+ };
+ } );
+ } )
+ )
+ .then( ( results ) => {
+ const combinedProducts = results.flatMap(
+ ( result ) => result.products
+ );
+
+ setAllProducts( combinedProducts );
+
+ setSearchResultsCount( {
+ extensions: results.find(
+ ( i ) => i.type === 'extension'
+ )?.totalProducts,
+ themes: results.find( ( i ) => i.type === 'theme' )
+ ?.totalProducts,
+ 'business-services': results.find(
+ ( i ) => i.type === 'business-service'
+ )?.totalProducts,
+ } );
+
+ results.forEach( ( result ) => {
+ switch ( result.type ) {
+ case ProductType.extension:
+ setTotalPagesExtensions( result.totalPages );
+ break;
+ case ProductType.theme:
+ setTotalPagesThemes( result.totalPages );
+ break;
+ case ProductType.businessService:
+ setTotalPagesBusinessServices(
+ result.totalPages
+ );
+ break;
+ }
+ } );
+
+ searchCompleteAnnouncement(
+ results.reduce( ( acc, curr ) => {
+ return acc + curr.totalProducts;
+ }, 0 )
+ );
+ } )
+ .catch( () => {
+ setAllProducts( [] );
+ } )
+ .finally( () => {
+ setIsLoading( false );
+ } );
+ }
+
+ return () => {
+ abortControllers.forEach( ( controller ) => {
+ controller.abort();
+ } );
+ };
+ }, [
+ query.tab,
query.term,
query.category,
- query?.tab,
+ setHasBusinessServices,
setIsLoading,
- query?.section,
+ setSearchResultsCount,
+ currentPage,
] );
+ // Filter the products based on the selected tab
+ useEffect( () => {
+ let filtered: Product[] | null;
+ switch ( selectedTab ) {
+ case 'extensions':
+ filtered = allProducts.filter(
+ ( p ) => p.type === ProductType.extension
+ );
+ break;
+ case 'themes':
+ filtered = allProducts.filter(
+ ( p ) => p.type === ProductType.theme
+ );
+ break;
+ case 'business-services':
+ filtered = allProducts.filter(
+ ( p ) => p.type === ProductType.businessService
+ );
+ break;
+ default:
+ filtered = [];
+ }
+ setFilteredProducts( filtered );
+ }, [ selectedTab, allProducts ] );
+
+ // Record tab view events when the query changes
+ useEffect( () => {
+ const marketplaceViewProps = {
+ view: query?.tab,
+ search_term: query?.term,
+ product_type: query?.section,
+ category: query?.category,
+ };
+ recordMarketplaceView( marketplaceViewProps );
+ recordLegacyTabView( marketplaceViewProps );
+ }, [ query?.tab, query?.term, query?.section, query?.category ] );
+
+ // Reset current page when tab, term, or category changes
+ useEffect( () => {
+ setCurrentPage( 1 );
+ setFirstNewProductId( 0 );
+ }, [ selectedTab, query?.category, query?.term ] );
+
+ // Maintain product focus for accessibility
+ useEffect( () => {
+ if ( firstNewProductId ) {
+ setTimeout( () => {
+ const firstNewProduct = document.getElementById(
+ `product-${ firstNewProductId }`
+ );
+ if ( firstNewProduct ) {
+ firstNewProduct.focus();
+ }
+ }, 0 );
+ }
+ }, [ firstNewProductId ] );
+
const renderContent = (): JSX.Element => {
switch ( selectedTab ) {
case 'extensions':
- return (
-
- );
case 'themes':
- return (
-
- );
case 'business-services':
return (
- );
- case 'search':
- return (
-
);
case 'discover':
@@ -184,10 +392,29 @@ export default function Content(): JSX.Element {
}
};
+ const shouldShowLoadMoreButton = () => {
+ if ( ! query.category || query.category === '_all' ) {
+ // Check against total pages for the selected tab
+ switch ( selectedTab ) {
+ case 'extensions':
+ return currentPage < totalPagesExtensions;
+ case 'themes':
+ return currentPage < totalPagesThemes;
+ case 'business-services':
+ return currentPage < totalPagesBusinessServices;
+ default:
+ return false;
+ }
+ } else {
+ // Check against totalPagesCategory for specific category
+ return currentPage < totalPagesCategory;
+ }
+ };
+
return (
-
+
{ selectedTab !== 'business-services' &&
selectedTab !== 'my-subscriptions' &&
}
{ selectedTab !== 'business-services' &&
}
@@ -197,11 +424,15 @@ export default function Content(): JSX.Element {
{ selectedTab !== 'business-services' && (
) }
- { selectedTab !== 'business-services' && (
-
- ) }
{ renderContent() }
+ { ! isLoading && shouldShowLoadMoreButton() && (
+
+ ) }
);
}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/header/header.scss b/plugins/woocommerce-admin/client/marketplace/components/header/header.scss
index 11616f62a7f..f4e14f0ff23 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/header/header.scss
+++ b/plugins/woocommerce-admin/client/marketplace/components/header/header.scss
@@ -5,6 +5,7 @@
background: #fff;
border-bottom: 1px solid $gutenberg-gray-300;
display: grid;
+ gap: $medium-gap;
grid-template: "mktpl-title mktpl-search mktpl-meta" 60px
"mktpl-tabs mktpl-tabs mktpl-tabs" auto / 1fr 320px 36px;
padding: 0 $content-spacing-large;
@@ -73,17 +74,3 @@
padding: 0 $content-spacing-small;
}
}
-
-.woocommerce-marketplace__search {
- margin-right: $medium-gap;
- margin-top: 10px;
-
- input[type="search"] {
- all: unset;
- flex-grow: 1;
- }
-
- @media (width <= $breakpoint-medium) {
- margin: $content-spacing-small;
- }
-}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/load-more-button/load-more-button.tsx b/plugins/woocommerce-admin/client/marketplace/components/load-more-button/load-more-button.tsx
new file mode 100644
index 00000000000..07fb2ea6c48
--- /dev/null
+++ b/plugins/woocommerce-admin/client/marketplace/components/load-more-button/load-more-button.tsx
@@ -0,0 +1,37 @@
+/**
+ * External dependencies
+ */
+import { Button } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { speak } from '@wordpress/a11y';
+import { queueRecordEvent } from '@woocommerce/tracks';
+
+interface LoadMoreProps {
+ onLoadMore: () => void;
+ isBusy: boolean;
+ disabled: boolean;
+}
+
+export default function LoadMoreButton( props: LoadMoreProps ) {
+ const { onLoadMore, isBusy, disabled } = props;
+ function handleClick() {
+ queueRecordEvent( 'marketplace_load_more_button_clicked', {} );
+ onLoadMore();
+ }
+
+ if ( isBusy ) {
+ speak( __( 'Loading more products', 'woocommerce' ) );
+ }
+
+ return (
+
+ );
+}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.tsx b/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.tsx
index 79585e83189..00ba4a2d0e7 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.tsx
@@ -191,6 +191,8 @@ function ProductCard( props: ProductCardProps ): JSX.Element {
return (
diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-list-content/no-results.tsx b/plugins/woocommerce-admin/client/marketplace/components/product-list-content/no-results.tsx
index da81e0d8059..9ecdd54c98a 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/product-list-content/no-results.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/components/product-list-content/no-results.tsx
@@ -3,7 +3,6 @@
*/
import { __ } from '@wordpress/i18n';
import { useEffect, useState } from '@wordpress/element';
-import { useQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
@@ -22,8 +21,6 @@ export default function NoResults( props: {
} ): JSX.Element {
const [ productGroups, setProductGroups ] = useState< ProductGroup[] >();
const [ isLoading, setIsLoading ] = useState( false );
- const query = useQuery();
- const showCategorySelector = query.tab === 'search' && query.section;
const productGroupsForSearchType = {
[ SearchResultType.all ]: [
'most-popular',
@@ -123,10 +120,6 @@ export default function NoResults( props: {
}
function categorySelector() {
- if ( ! showCategorySelector ) {
- return <>>;
- }
-
if ( props.type === SearchResultType.all ) {
return <>>;
}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts b/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts
index 5008cef836a..9ee660ddb08 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts
+++ b/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts
@@ -1,5 +1,7 @@
export type SearchAPIJSONType = {
products: Array< SearchAPIProductType >;
+ total_pages: number;
+ total_products: number;
};
export type SearchAPIProductType = {
diff --git a/plugins/woocommerce-admin/client/marketplace/components/products/products.scss b/plugins/woocommerce-admin/client/marketplace/components/products/products.scss
index 2d92bfd5259..4fb5f89218c 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/products/products.scss
+++ b/plugins/woocommerce-admin/client/marketplace/components/products/products.scss
@@ -9,10 +9,21 @@
}
}
+
.woocommerce-marketplace__sub-header {
display: flex;
-
- .woocommerce-marketplace__customize-your-store-button {
- margin: 16px 0 6px auto;
- }
+ align-items: center;
+ justify-content: space-between;
+ gap: 32px;
}
+
+.woocommerce-marketplace__sub-header__categories {
+ flex: 1;
+ overflow-x: auto;
+ position: relative;
+}
+
+.woocommerce-marketplace__customize-your-store-button {
+ flex-shrink: 0;
+}
+
diff --git a/plugins/woocommerce-admin/client/marketplace/components/products/products.tsx b/plugins/woocommerce-admin/client/marketplace/components/products/products.tsx
index 8ecd211e4d2..a078e991198 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/products/products.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/components/products/products.tsx
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { __, _n, sprintf } from '@wordpress/i18n';
+import { __ } from '@wordpress/i18n';
import {
createInterpolateElement,
useContext,
@@ -24,7 +24,6 @@ import ProductListContent from '../product-list-content/product-list-content';
import ProductLoader from '../product-loader/product-loader';
import NoResults from '../product-list-content/no-results';
import { Product, ProductType, SearchResultType } from '../product-list/types';
-import { MARKETPLACE_ITEMS_PER_PAGE } from '../constants';
import { ADMIN_URL } from '~/utils/admin-settings';
import { ThemeSwitchWarningModal } from '~/customize-store/intro/warning-modals';
@@ -54,12 +53,10 @@ const LABELS = {
export default function Products( props: ProductsProps ) {
const marketplaceContextValue = useContext( MarketplaceContext );
- const { isLoading, selectedTab } = marketplaceContextValue;
+ const { isLoading } = marketplaceContextValue;
const label = LABELS[ props.type ].label;
- const singularLabel = LABELS[ props.type ].singularLabel;
const query = useQuery();
const category = query?.category;
- const perPage = props.perPage ?? MARKETPLACE_ITEMS_PER_PAGE;
interface Theme {
stylesheet?: string;
}
@@ -94,42 +91,30 @@ export default function Products( props: ProductsProps ) {
}
// Store the total number of products before we slice it later.
- const productTotalCount = props.products?.length ?? 0;
- const products = props.products?.slice( 0, perPage ) ?? [];
-
- let title = sprintf(
- // translators: %s: plural item type (e.g. extensions, themes)
- __( '0 %s found', 'woocommerce' ),
- label
- );
-
- if ( productTotalCount > 0 ) {
- title = sprintf(
- // translators: %1$s: number of items, %2$s: singular item label, %3$s: plural item label
- _n( '%1$s %2$s', '%1$s %3$s', productTotalCount, 'woocommerce' ),
- productTotalCount,
- singularLabel,
- label
- );
- }
+ const products = props.products ?? [];
const labelForClassName =
label === 'business services' ? 'business-services' : label;
const baseContainerClass = 'woocommerce-marketplace__search-';
- const baseProductListTitleClass = 'product-list-title--';
const containerClassName = clsx( baseContainerClass + labelForClassName );
- const productListTitleClassName = clsx(
- 'woocommerce-marketplace__product-list-title',
- baseContainerClass + baseProductListTitleClass + labelForClassName,
- { 'is-loading': isLoading }
- );
const viewAllButonClassName = clsx(
'woocommerce-marketplace__view-all-button',
baseContainerClass + 'button-' + labelForClassName
);
+ if ( isLoading ) {
+ return (
+ <>
+ { props.categorySelector && (
+
+ ) }
+
+ >
+ );
+ }
+
if ( products.length === 0 ) {
let type = SearchResultType.all;
@@ -154,28 +139,14 @@ export default function Products( props: ProductsProps ) {
: ''
);
- if ( isLoading ) {
- return (
- <>
- { props.categorySelector && (
-
- ) }
-
- >
- );
- }
-
return (
- { selectedTab === 'search' && (
-
- { isLoading ? ' ' : title }
-
- ) }
-
- { props.categorySelector && (
-
- ) }
+
+
{ isModalOpen && (
product.type === ProductType.extension
- );
- const themeList = props.products.filter(
- ( product ) => product.type === ProductType.theme
- );
- const businessServiceList = props.products.filter(
- ( product ) => product.type === ProductType.businessService
- );
-
- const hasExtensions = extensionList.length > 0;
- const hasThemes = themeList.length > 0;
- const hasBusinessServices = businessServiceList.length > 0;
- const hasOnlyExtensions =
- hasExtensions && ! hasThemes && ! hasBusinessServices;
- const hasOnlyThemes = hasThemes && ! hasExtensions && ! hasBusinessServices;
- const hasOnlyBusinessServices =
- hasBusinessServices && ! hasExtensions && ! hasThemes;
-
- const marketplaceContextValue = useContext( MarketplaceContext );
- const { isLoading, hasBusinessServices: canShowBusinessServices } =
- marketplaceContextValue;
-
- const query = useQuery();
- const showCategorySelector = query.section ? true : false;
- const searchTerm = query.term ? query.term : '';
-
- type Overrides = {
- categorySelector?: boolean;
- showAllButton?: boolean;
- perPage?: number;
- };
-
- function productsComponent(
- products: Product[],
- type: ProductType,
- overrides: Overrides = {}
- ) {
- return (
-
- );
- }
-
- function extensionsComponent( overrides: Overrides = {} ) {
- return productsComponent(
- extensionList,
- ProductType.extension,
- overrides
- );
- }
-
- function themesComponent( overrides: Overrides = {} ) {
- return productsComponent( themeList, ProductType.theme, overrides );
- }
-
- function businessServicesComponent( overrides: Overrides = {} ) {
- return productsComponent(
- businessServiceList,
- ProductType.businessService,
- overrides
- );
- }
-
- const content = () => {
- if ( query?.section === SearchResultType.extension ) {
- return extensionsComponent( { showAllButton: false } );
- }
-
- if ( query?.section === SearchResultType.theme ) {
- return themesComponent( { showAllButton: false } );
- }
-
- if ( query?.section === SearchResultType.businessService ) {
- return businessServicesComponent( { showAllButton: false } );
- }
-
- // Components can handle their isLoading state. So we can put all three on the page.
- if ( isLoading ) {
- return (
- <>
- { extensionsComponent() }
- { themesComponent() }
- { businessServicesComponent() }
- >
- );
- }
-
- // If we did finish loading items, and there are no results, show the no results component.
- if (
- ! isLoading &&
- ! hasExtensions &&
- ! hasThemes &&
- ! hasBusinessServices
- ) {
- return (
-
- );
- }
-
- // If we're done loading, we can put these components on the page.
- return (
- <>
- { hasExtensions
- ? extensionsComponent( {
- categorySelector: hasOnlyExtensions || undefined,
- showAllButton: hasOnlyExtensions
- ? false
- : undefined,
- perPage: hasOnlyExtensions
- ? MARKETPLACE_ITEMS_PER_PAGE
- : MARKETPLACE_SEARCH_RESULTS_PER_PAGE,
- } )
- : null }
- { hasThemes
- ? themesComponent( {
- categorySelector: hasOnlyThemes || undefined,
- showAllButton: hasOnlyThemes ? false : undefined,
- perPage: hasOnlyThemes
- ? MARKETPLACE_ITEMS_PER_PAGE
- : MARKETPLACE_SEARCH_RESULTS_PER_PAGE,
- } )
- : null }
- { hasBusinessServices
- ? businessServicesComponent( {
- categorySelector:
- hasOnlyBusinessServices || undefined,
- showAllButton: hasOnlyBusinessServices
- ? false
- : undefined,
- perPage: hasOnlyBusinessServices
- ? MARKETPLACE_ITEMS_PER_PAGE
- : MARKETPLACE_SEARCH_RESULTS_PER_PAGE,
- } )
- : null }
- >
- );
- };
-
- return (
-
- { content() }
-
- );
-}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/search/search.scss b/plugins/woocommerce-admin/client/marketplace/components/search/search.scss
index e6725b7ca2d..0fb878f214f 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/search/search.scss
+++ b/plugins/woocommerce-admin/client/marketplace/components/search/search.scss
@@ -2,29 +2,15 @@
.woocommerce-marketplace__search {
grid-area: mktpl-search;
- background: $gutenberg-gray-100;
- border: 1.5px solid transparent;
- border-radius: 2px;
- display: flex;
- height: 40px;
- padding: 4px 8px 4px 12px;
-
- input[type="search"] {
- all: unset;
- flex-grow: 1;
- }
-
- &:focus-within {
- background: #fff;
- border-color: var(--wp-admin-theme-color, #3858e9);
- }
+ margin-top: 15px;
+ width: 320px;
@media (width <= $breakpoint-medium) {
margin: $grid-unit-20 $grid-unit-20 $grid-unit-10 $grid-unit-20;
+ width: calc(100% - $grid-unit-20 * 2);
+
+ .components-input-control__input {
+ font-size: 13px !important;
+ }
}
}
-
-.woocommerce-marketplace__search-button {
- all: unset;
- cursor: pointer;
-}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx b/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx
index e03582ca4e8..f9572c7c7fa 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx
@@ -2,26 +2,20 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
-import { Icon, search } from '@wordpress/icons';
-import { useContext, useEffect, useState } from '@wordpress/element';
+import { useEffect, useState } from '@wordpress/element';
import { navigateTo, getNewPath, useQuery } from '@woocommerce/navigation';
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
+// eslint-disable-next-line @woocommerce/dependency-group
+import { SearchControl } from '@wordpress/components';
+// The @ts-ignore is needed because the SearchControl types are not exported from the @wordpress/components package,
+// even though the component itself is. This is likely due to an older version of the package being used.
/**
* Internal dependencies
*/
import './search.scss';
import { MARKETPLACE_PATH } from '../constants';
-import { MarketplaceContext } from '../../contexts/marketplace-context';
-
-const searchPlaceholder = __(
- 'Search for extensions, themes, and business services',
- 'woocommerce'
-);
-
-const searchPlaceholderNoBusinessServices = __(
- 'Search for extensions and themes',
- 'woocommerce'
-);
/**
* Search component.
@@ -30,14 +24,10 @@ const searchPlaceholderNoBusinessServices = __(
*/
function Search(): JSX.Element {
const [ searchTerm, setSearchTerm ] = useState( '' );
- const { hasBusinessServices } = useContext( MarketplaceContext );
+ const searchPlaceholder = __( 'Search Marketplace', 'woocommerce' );
const query = useQuery();
- const placeholder = hasBusinessServices
- ? searchPlaceholder
- : searchPlaceholderNoBusinessServices;
-
useEffect( () => {
if ( query.term ) {
setSearchTerm( query.term );
@@ -46,21 +36,16 @@ function Search(): JSX.Element {
}
}, [ query.term ] );
- useEffect( () => {
- if ( query.tab !== 'search' ) {
- setSearchTerm( '' );
- }
- }, [ query.tab ] );
-
const runSearch = () => {
- const term = searchTerm.trim();
+ const newQuery: { term?: string; tab?: string } = query;
- const newQuery: { term?: string; tab?: string } = {};
- if ( term !== '' ) {
- newQuery.term = term;
- newQuery.tab = 'search';
+ // If we're on 'Discover' or 'My subscriptions' when a search is initiated, move to the extensions tab
+ if ( ! newQuery.tab || newQuery.tab === 'my-subscriptions' ) {
+ newQuery.tab = 'extensions';
}
+ newQuery.term = searchTerm.trim();
+
// When the search term changes, we reset the query string on purpose.
navigateTo( {
url: getNewPath( newQuery, MARKETPLACE_PATH, {} ),
@@ -69,12 +54,6 @@ function Search(): JSX.Element {
return [];
};
- const handleInputChange = (
- event: React.ChangeEvent< HTMLInputElement >
- ) => {
- setSearchTerm( event.target.value );
- };
-
const handleKeyUp = ( event: { key: string } ) => {
if ( event.key === 'Enter' ) {
runSearch();
@@ -86,32 +65,14 @@ function Search(): JSX.Element {
};
return (
-
-
-
-
-
+
);
}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss b/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss
index 008fe4e1b84..3a8e7c152e6 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss
+++ b/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss
@@ -45,6 +45,18 @@
text-align: center;
z-index: 26;
}
+
+ &__update-count-extensions,
+ &__update-count-themes,
+ &__update-count-business-services {
+ background-color: $gutenberg-gray-300;
+ color: $gutenberg-gray-700;
+
+ &.is-active {
+ background-color: #000;
+ color: #fff;
+ }
+ }
}
@media (width <= $breakpoint-medium) {
diff --git a/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.tsx b/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.tsx
index 1f852a2b982..052a06df435 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.tsx
@@ -2,7 +2,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
-import { useContext, useEffect, useState } from '@wordpress/element';
+import { useContext, useEffect, useState, useMemo } from '@wordpress/element';
import { Button } from '@wordpress/components';
import clsx from 'clsx';
import { getNewPath, navigateTo, useQuery } from '@woocommerce/navigation';
@@ -35,63 +35,26 @@ interface Tabs {
const wccomSettings = getAdminSetting( 'wccomHelper', {} );
const wooUpdateCount = wccomSettings?.wooUpdateCount ?? 0;
-const tabs: Tabs = {
- search: {
- name: 'search',
- title: __( 'Search results', 'woocommerce' ),
- showUpdateCount: false,
- updateCount: 0,
- },
- discover: {
- name: 'discover',
- title: __( 'Discover', 'woocommerce' ),
- showUpdateCount: false,
- updateCount: 0,
- },
- extensions: {
- name: 'extensions',
- title: __( 'Extensions', 'woocommerce' ),
- showUpdateCount: false,
- updateCount: 0,
- },
- themes: {
- name: 'themes',
- title: __( 'Themes', 'woocommerce' ),
- showUpdateCount: false,
- updateCount: 0,
- },
- 'business-services': {
- name: 'business-services',
- title: __( 'Business services', 'woocommerce' ),
- showUpdateCount: false,
- updateCount: 0,
- },
- 'my-subscriptions': {
- name: 'my-subscriptions',
- title: __( 'My subscriptions', 'woocommerce' ),
- showUpdateCount: true,
- updateCount: wooUpdateCount,
- },
-};
-
-const setUrlTabParam = ( tabKey: string ) => {
+const setUrlTabParam = ( tabKey: string, query: Record< string, string > ) => {
+ const term = query.term ? { term: query.term.trim() } : {};
navigateTo( {
url: getNewPath(
{ tab: tabKey === DEFAULT_TAB_KEY ? undefined : tabKey },
MARKETPLACE_PATH,
- {}
+ term
),
} );
};
-const getVisibleTabs = ( selectedTab: string, hasBusinessServices = false ) => {
+const getVisibleTabs = (
+ selectedTab: string,
+ hasBusinessServices = false,
+ tabs: Tabs
+) => {
if ( selectedTab === '' ) {
return tabs;
}
const currentVisibleTabs = { ...tabs };
- if ( selectedTab !== 'search' ) {
- delete currentVisibleTabs.search;
- }
if ( ! hasBusinessServices ) {
delete currentVisibleTabs[ 'business-services' ];
}
@@ -101,7 +64,9 @@ const getVisibleTabs = ( selectedTab: string, hasBusinessServices = false ) => {
const renderTabs = (
marketplaceContextValue: MarketplaceContextType,
- visibleTabs: Tabs
+ visibleTabs: Tabs,
+ tabs: Tabs,
+ query: Record< string, string >
) => {
const { selectedTab, setSelectedTab } = marketplaceContextValue;
@@ -110,7 +75,7 @@ const renderTabs = (
return;
}
setSelectedTab( tabKey );
- setUrlTabParam( tabKey );
+ setUrlTabParam( tabKey, query );
};
const tabContent = [];
@@ -143,7 +108,15 @@ const renderTabs = (
{ tabs[ tabKey ]?.title }
{ tabs[ tabKey ]?.showUpdateCount &&
tabs[ tabKey ]?.updateCount > 0 && (
-
+
{ tabs[ tabKey ]?.updateCount }
) }
@@ -157,23 +130,70 @@ const renderTabs = (
const Tabs = ( props: TabsProps ): JSX.Element => {
const { additionalClassNames } = props;
const marketplaceContextValue = useContext( MarketplaceContext );
- const { selectedTab, setSelectedTab, hasBusinessServices } =
+ const { selectedTab, isLoading, setSelectedTab, hasBusinessServices } =
marketplaceContextValue;
- const [ visibleTabs, setVisibleTabs ] = useState( getVisibleTabs( '' ) );
+ const { searchResultsCount } = marketplaceContextValue;
const query: Record< string, string > = useQuery();
+ const tabs: Tabs = useMemo(
+ () => ( {
+ discover: {
+ name: 'discover',
+ title: __( 'Discover', 'woocommerce' ),
+ showUpdateCount: false,
+ updateCount: 0,
+ },
+ extensions: {
+ name: 'extensions',
+ title: __( 'Extensions', 'woocommerce' ),
+ showUpdateCount: !! query.term && ! isLoading,
+ updateCount: searchResultsCount.extensions,
+ },
+ themes: {
+ name: 'themes',
+ title: __( 'Themes', 'woocommerce' ),
+ showUpdateCount: !! query.term && ! isLoading,
+ updateCount: searchResultsCount.themes,
+ },
+ 'business-services': {
+ name: 'business-services',
+ title: __( 'Business services', 'woocommerce' ),
+ showUpdateCount: !! query.term && ! isLoading,
+ updateCount: searchResultsCount[ 'business-services' ],
+ },
+ 'my-subscriptions': {
+ name: 'my-subscriptions',
+ title: __( 'My subscriptions', 'woocommerce' ),
+ showUpdateCount: true,
+ updateCount: wooUpdateCount,
+ },
+ } ),
+ [ query, isLoading, searchResultsCount ]
+ );
+
+ const [ visibleTabs, setVisibleTabs ] = useState(
+ getVisibleTabs( '', false, tabs )
+ );
+
useEffect( () => {
if ( query?.tab && tabs[ query.tab ] ) {
setSelectedTab( query.tab );
} else if ( Object.keys( query ).length > 0 ) {
setSelectedTab( DEFAULT_TAB_KEY );
}
- }, [ query, setSelectedTab ] );
+ }, [ query, setSelectedTab, tabs ] );
useEffect( () => {
- setVisibleTabs( getVisibleTabs( selectedTab, hasBusinessServices ) );
- }, [ selectedTab, hasBusinessServices ] );
+ setVisibleTabs(
+ getVisibleTabs( selectedTab, hasBusinessServices, tabs )
+ );
+
+ if ( selectedTab === 'business-services' && ! hasBusinessServices ) {
+ setUrlTabParam( 'extensions', query );
+ }
+ }, [ selectedTab, hasBusinessServices, query, tabs ] );
+
return (
);
};
diff --git a/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx b/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx
index 53dd1bb5789..0631bf5725e 100644
--- a/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx
@@ -1,12 +1,17 @@
/**
* External dependencies
*/
-import { useState, useEffect, createContext } from '@wordpress/element';
+import {
+ useState,
+ useEffect,
+ useCallback,
+ createContext,
+} from '@wordpress/element';
/**
* Internal dependencies
*/
-import { MarketplaceContextType } from './types';
+import { SearchResultsCountType, MarketplaceContextType } from './types';
import { getAdminSetting } from '../../utils/admin-settings';
export const MarketplaceContext = createContext< MarketplaceContextType >( {
@@ -18,6 +23,12 @@ export const MarketplaceContext = createContext< MarketplaceContextType >( {
addInstalledProduct: () => {},
hasBusinessServices: false,
setHasBusinessServices: () => {},
+ searchResultsCount: {
+ extensions: 0,
+ themes: 0,
+ 'business-services': 0,
+ },
+ setSearchResultsCount: () => {},
} );
export function MarketplaceContextProvider( props: {
@@ -29,6 +40,22 @@ export function MarketplaceContextProvider( props: {
[]
);
const [ hasBusinessServices, setHasBusinessServices ] = useState( false );
+ const [ searchResultsCount, setSearchResultsCountState ] =
+ useState< SearchResultsCountType >( {
+ extensions: 0,
+ themes: 0,
+ 'business-services': 0,
+ } );
+
+ const setSearchResultsCount = useCallback(
+ ( updatedCounts: Partial< SearchResultsCountType > ) => {
+ setSearchResultsCountState( ( prev ) => ( {
+ ...prev,
+ ...updatedCounts,
+ } ) );
+ },
+ []
+ );
/**
* Knowing installed products will help us to determine which products
@@ -59,6 +86,8 @@ export function MarketplaceContextProvider( props: {
addInstalledProduct,
hasBusinessServices,
setHasBusinessServices,
+ searchResultsCount,
+ setSearchResultsCount,
};
return (
diff --git a/plugins/woocommerce-admin/client/marketplace/contexts/types.ts b/plugins/woocommerce-admin/client/marketplace/contexts/types.ts
index 969ce88d2e1..3240729871e 100644
--- a/plugins/woocommerce-admin/client/marketplace/contexts/types.ts
+++ b/plugins/woocommerce-admin/client/marketplace/contexts/types.ts
@@ -8,6 +8,12 @@ import { Options } from '@wordpress/notices';
*/
import { Subscription } from '../components/my-subscriptions/types';
+export interface SearchResultsCountType {
+ extensions: number;
+ themes: number;
+ 'business-services': number;
+}
+
export type MarketplaceContextType = {
isLoading: boolean;
setIsLoading: ( isLoading: boolean ) => void;
@@ -17,6 +23,10 @@ export type MarketplaceContextType = {
addInstalledProduct: ( slug: string ) => void;
hasBusinessServices: boolean;
setHasBusinessServices: ( hasBusinessServices: boolean ) => void;
+ searchResultsCount: SearchResultsCountType;
+ setSearchResultsCount: (
+ updatedCounts: Partial< SearchResultsCountType >
+ ) => void;
};
export type SubscriptionsContextType = {
diff --git a/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx b/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx
index d7fbc982c15..a063490eefc 100644
--- a/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx
@@ -107,7 +107,11 @@ async function fetchJsonWithCache(
async function fetchSearchResults(
params: URLSearchParams,
abortSignal?: AbortSignal
-): Promise< Product[] > {
+): Promise< {
+ products: Product[];
+ totalPages: number;
+ totalProducts: number;
+} > {
const url =
MARKETPLACE_HOST +
MARKETPLACE_SEARCH_API_PATH +
@@ -151,9 +155,12 @@ async function fetchSearchResults(
};
}
);
- resolve( products );
+ const totalPages = ( json as SearchAPIJSONType ).total_pages;
+ const totalProducts = ( json as SearchAPIJSONType )
+ .total_products;
+ resolve( { products, totalPages, totalProducts } );
} )
- .catch( () => reject );
+ .catch( reject );
} );
}
@@ -174,6 +181,17 @@ async function fetchDiscoverPageData(): Promise< ProductGroup[] > {
}
}
+function getProductType( tab: string ): ProductType {
+ switch ( tab ) {
+ case 'themes':
+ return ProductType.theme;
+ case 'business-services':
+ return ProductType.businessService;
+ default:
+ return ProductType.extension;
+ }
+}
+
function fetchCategories( type: ProductType ): Promise< CategoryAPIItem[] > {
const url = new URL( MARKETPLACE_HOST + MARKETPLACE_CATEGORY_API_PATH );
@@ -478,6 +496,7 @@ export {
fetchCategories,
fetchDiscoverPageData,
fetchSearchResults,
+ getProductType,
fetchSubscriptions,
refreshSubscriptions,
getInstallUrl,
diff --git a/plugins/woocommerce-admin/client/marketplace/utils/tracking.ts b/plugins/woocommerce-admin/client/marketplace/utils/tracking.ts
index 6729c824c55..8b08027770e 100644
--- a/plugins/woocommerce-admin/client/marketplace/utils/tracking.ts
+++ b/plugins/woocommerce-admin/client/marketplace/utils/tracking.ts
@@ -42,11 +42,6 @@ function recordMarketplaceView( props: MarketplaceViewProps ) {
eventProps.category = '_all';
}
- // User clicks the `View All` button on search results
- if ( view && view === 'search' && product_type && ! category ) {
- eventProps.category = '_all';
- }
-
recordEvent( 'marketplace_view', eventProps );
}
@@ -80,11 +75,6 @@ function recordLegacyTabView( props: MarketplaceViewProps ) {
case 'themes':
oldEventProps.section = 'themes';
break;
- case 'search':
- oldEventName = 'extensions_view_search';
- oldEventProps.section = view;
- oldEventProps.search_term = search_term || '';
- break;
case 'my-subscriptions':
oldEventName = 'subscriptions_view';
oldEventProps.section = 'helper';
diff --git a/plugins/woocommerce/changelog/51309-update-21591-in-app-category-selector b/plugins/woocommerce/changelog/51309-update-21591-in-app-category-selector
new file mode 100644
index 00000000000..504e3d4d2d2
--- /dev/null
+++ b/plugins/woocommerce/changelog/51309-update-21591-in-app-category-selector
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+Update In-App Marketplace category selector
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/51313-update-wccom-21595-in-app-search-component b/plugins/woocommerce/changelog/51313-update-wccom-21595-in-app-search-component
new file mode 100644
index 00000000000..42a06d9bf86
--- /dev/null
+++ b/plugins/woocommerce/changelog/51313-update-wccom-21595-in-app-search-component
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Replace marketplace search component with SearchControl from @wordpress/components
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/51342-fix-21596-search-loading-state b/plugins/woocommerce/changelog/51342-fix-21596-search-loading-state
new file mode 100644
index 00000000000..a55cd0e6601
--- /dev/null
+++ b/plugins/woocommerce/changelog/51342-fix-21596-search-loading-state
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix the loading state for the In-App Marketplace search
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/51434-add-wccom-21568-in-app-load-more-button b/plugins/woocommerce/changelog/51434-add-wccom-21568-in-app-load-more-button
new file mode 100644
index 00000000000..50f6c905990
--- /dev/null
+++ b/plugins/woocommerce/changelog/51434-add-wccom-21568-in-app-load-more-button
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Added a Load More button to product lists on the Extensions page, to request additional search results from WooCommerce.com.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/refactor-wccom-21576-search-tab b/plugins/woocommerce/changelog/refactor-wccom-21576-search-tab
new file mode 100644
index 00000000000..ba8046f3451
--- /dev/null
+++ b/plugins/woocommerce/changelog/refactor-wccom-21576-search-tab
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Change the way search results are displayed in the in-app marketplace
diff --git a/plugins/woocommerce/changelog/tweak-21597-in-app-search-results-count b/plugins/woocommerce/changelog/tweak-21597-in-app-search-results-count
new file mode 100644
index 00000000000..b0edda219c3
--- /dev/null
+++ b/plugins/woocommerce/changelog/tweak-21597-in-app-search-results-count
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add search result counts to the in-app marketplace header tabs (Extensions area)