In app search improvements feature branch (#51413)
* Add search results count to the in-app marketplace (#51266)
* Add searchResults to context
* Use setSearchResults in Content
* Add ribbons to the tabs
* Changelog
* Use setState as the function name
* Only show ribbon counts when there's an active search
* Refactor how 'setSearchResultsCount' is used (h/t @mcliwanow)
* Don't populate initial search results
* Unify css styling
* Marketplace: bring back the loading state (#51342)
* Marketplace: bring back the loading state
* Add changefile(s) from automation for the following project(s): woocommerce
---------
Co-authored-by: github-actions <github-actions@github.com>
* Remove in-app marketplace Search results tab and unify results into existing tabs (#51297)
* Remove search results component and any references to it
* Persist current tab for searching, or default to extensions if tab is not set
* Persist term when switching across tabs
* Lint
* When a search is initiated, fetch all categories to keep the tab counts up to date.
The necessary filtering to display data to the current screen will be performed on the frontend.
* Apply correct colors to the tabs, as per design
* Beyond query.term, also rely on isLoading so that search result counts don't jump
* Address an issue when the user searches for something that returns no results in the business services tab
* Changelog
* Addressed :)
* Change key to category
* Fix category filter being broken
Whenever a category is requested, we need to do an additional request with the category param being the current category (overriding extensions/theme/business services).
Ideally the backend API would make a distinction between type (theme/extension/business service) and category, but this hack should do for now.
* Lint
* Remove unused variables h/t @KokkieH
* Lint
* Revert "Lint"
This reverts commit 0b2d2dca6d
.
* Actually fix lint without introducing infinite loop
Reproducible at http://localhost:8080/wp-admin/admin.php?page=wc-admin&term=payments&tab=extensions&path=%2Fextensions&category=customer-service
* Show category selector even if there are no results displayed
* Update comment to be less misleading
* Query isn't used here
* Update Marketplace search component (#51313)
* Update Search placeholder text
* Replace search component with one from @wordpress/components
* Make mobile search field font consistent with desktop
* Add changefile(s) from automation for the following project(s): woocommerce
* Handle import errors for SearchControl component
---------
Co-authored-by: github-actions <github-actions@github.com>
* Marketplace: update category selector (#51309)
* Marketplace: update category selector
Remove the dropdown on the desktop view and show all items, even if
overflowing. Added helper buttons to scroll to the right to show more.
* Add changefile(s) from automation for the following project(s): woocommerce
* Marketplace: remove category sroll helpers from tabindex
GitHub:
https://github.com/woocommerce/woocommerce/pull/51309/files#r1758448638
* Marketplace: Remove selectedTab reference from product.tsx
This is probably included due to the merge conflict
* Marketplace: tweak category scroll button narrower
---------
Co-authored-by: github-actions <github-actions@github.com>
* Lint
* Fix 2 lint errors
* Fix another lint error (useMemo) h/t @KokkieH
* Add load more button in-app (#51434)
* Add additional fields returned by search API to marketplace types
Ensure components have access to additional fields
* Add LoadMoreButton component
* Only render Load More button if there are additional pages of results
* Fetch and display next page of results in Load More button is clicked
* Simplify renderContent function to have less repetition
- Hide load more button while fetching results
* Improve loading of new products
- Ensure keyboard focus goes to first new product after Load More is clicked
* Add changefile(s) from automation for the following project(s): woocommerce
* Add blank line to separate sections
* Set category param based on current tab when loading more products
* Improve busy-state screen reader text
Co-authored-by: Boro Sitnikovski <buritomath@gmail.com>
* Add missing dependency
* Move getProductType() function to functions.tsx
- Do not show load more button if isLoading state is true
---------
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Boro Sitnikovski <buritomath@gmail.com>
* Rework the values used with `setSearchResultsCount`
After https://github.com/Automattic/woocommerce.com/pull/21678/files we
get a `totalProducts` so we can re-use that.
Also remove setting the counts when paginating since we set them to the
total.
* Add search complete announcement h/t @KokkieH
* Show update count only if greater than 0 h/t @andfinally
* Switch to Extensions tab if on My subscriptions when searching
* yoda
---------
Co-authored-by: Cem Ünalan <raicem@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Herman <KokkieH@users.noreply.github.com>
This commit is contained in:
parent
4bc4649008
commit
ce66b55bc5
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<ul className="woocommerce-marketplace__category-selector">
|
||||
{ visibleItems.map( ( category ) => (
|
||||
<ul
|
||||
className="woocommerce-marketplace__category-selector"
|
||||
aria-label="Categories"
|
||||
ref={ categorySelectorRef }
|
||||
>
|
||||
{ categoriesToShow.map( ( category ) => (
|
||||
<li
|
||||
className="woocommerce-marketplace__category-item"
|
||||
key={ category.slug }
|
||||
ref={
|
||||
category.slug === selected?.slug
|
||||
? selectedCategoryRef
|
||||
: null
|
||||
}
|
||||
>
|
||||
<CategoryLink
|
||||
{ ...category }
|
||||
selected={ category.slug === selected?.slug }
|
||||
aria-current={ category.slug === selected?.slug }
|
||||
/>
|
||||
</li>
|
||||
) ) }
|
||||
<li className="woocommerce-marketplace__category-item">
|
||||
{ dropdownItems.length > 0 && (
|
||||
<CategoryDropdown
|
||||
type={ props.type }
|
||||
label={ __( 'More', 'woocommerce' ) }
|
||||
categories={ dropdownItems }
|
||||
buttonClassName={ clsx(
|
||||
'woocommerce-marketplace__category-item-button',
|
||||
{
|
||||
'woocommerce-marketplace__category-item-button--selected':
|
||||
isSelectedInDropdown(),
|
||||
}
|
||||
) }
|
||||
contentClassName="woocommerce-marketplace__category-item-content"
|
||||
arrowIconSize={ 20 }
|
||||
selected={ selected }
|
||||
/>
|
||||
) }
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="woocommerce-marketplace__category-selector--full-width">
|
||||
<CategoryDropdown
|
||||
type={ props.type }
|
||||
label={ mobileCategoryDropdownLabel() }
|
||||
categories={ visibleItems.concat( dropdownItems ) }
|
||||
categories={ categoriesToShow }
|
||||
buttonClassName="woocommerce-marketplace__category-dropdown-button"
|
||||
className="woocommerce-marketplace__category-dropdown"
|
||||
contentClassName="woocommerce-marketplace__category-dropdown-content"
|
||||
selected={ selected }
|
||||
/>
|
||||
</div>
|
||||
{ isOverflowing && (
|
||||
<>
|
||||
<button
|
||||
onClick={ scrollToPrevCategories }
|
||||
className="woocommerce-marketplace__category-navigation-button woocommerce-marketplace__category-navigation-button--prev"
|
||||
hidden={ scrollPosition === 'start' }
|
||||
aria-label="Scroll to previous categories"
|
||||
tabIndex={ -1 }
|
||||
>
|
||||
<Icon icon="arrow-left-alt2" />
|
||||
</button>
|
||||
<button
|
||||
onClick={ scrollToNextCategories }
|
||||
className="woocommerce-marketplace__category-navigation-button woocommerce-marketplace__category-navigation-button--next"
|
||||
hidden={ scrollPosition === 'end' }
|
||||
aria-label="Scroll to next categories"
|
||||
tabIndex={ -1 }
|
||||
>
|
||||
<Icon icon="arrow-right-alt2" />
|
||||
</button>
|
||||
</>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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,79 +33,157 @@ 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() );
|
||||
|
||||
categories.forEach( ( category: string, index ) => {
|
||||
const params = new URLSearchParams();
|
||||
if ( category !== '' ) {
|
||||
params.append( 'category', category );
|
||||
}
|
||||
|
||||
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 );
|
||||
}
|
||||
}
|
||||
const searchCompleteAnnouncement = ( count: number ): void => {
|
||||
speak(
|
||||
sprintf(
|
||||
// translators: %d is the number of products found.
|
||||
__( '%d products found', 'woocommerce' ),
|
||||
count
|
||||
)
|
||||
);
|
||||
return () => {
|
||||
abortControllers.forEach( ( controller ) => {
|
||||
controller.abort();
|
||||
} );
|
||||
};
|
||||
} );
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [] );
|
||||
|
||||
// Get the content for this screen
|
||||
useEffect( () => {
|
||||
const tagProductsWithType = (
|
||||
products: Product[],
|
||||
type: ProductType
|
||||
): Product[] => {
|
||||
return products.map( ( product ) => ( {
|
||||
...product,
|
||||
type,
|
||||
} ) );
|
||||
};
|
||||
|
||||
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
|
||||
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 ) => {
|
||||
setAllProducts( ( prevProducts ) => {
|
||||
const flattenedPrevProducts = Array.isArray(
|
||||
prevProducts[ 0 ]
|
||||
)
|
||||
? prevProducts.flat()
|
||||
: prevProducts;
|
||||
|
||||
const newProducts = productList.products.filter(
|
||||
( newProduct ) =>
|
||||
! flattenedPrevProducts.some(
|
||||
( prevProduct ) =>
|
||||
prevProduct.id === newProduct.id
|
||||
)
|
||||
);
|
||||
} 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' );
|
||||
|
||||
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( () => {
|
||||
speak( __( 'Error loading more products', 'woocommerce' ) );
|
||||
} )
|
||||
.finally( () => {
|
||||
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 );
|
||||
|
@ -106,69 +191,192 @@ export default function Content(): JSX.Element {
|
|||
params.append( 'country', wccomSettings.storeCountry );
|
||||
}
|
||||
|
||||
fetchSearchResults( params, abortController.signal )
|
||||
fetchSearchResults( params, abortControllers[ 0 ].signal )
|
||||
.then( ( productList ) => {
|
||||
setProducts( productList );
|
||||
setAllProducts( productList.products );
|
||||
setTotalPagesCategory( productList.totalPages );
|
||||
setSearchResultsCount( {
|
||||
[ query.tab ]: productList.totalProducts,
|
||||
} );
|
||||
|
||||
searchCompleteAnnouncement( productList.totalProducts );
|
||||
} )
|
||||
.catch( () => {
|
||||
setProducts( [] );
|
||||
setAllProducts( [] );
|
||||
} )
|
||||
.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
|
||||
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,
|
||||
setHasBusinessServices,
|
||||
setIsLoading,
|
||||
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 );
|
||||
setIsLoading( false );
|
||||
} );
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, [
|
||||
query.term,
|
||||
query.category,
|
||||
query?.tab,
|
||||
setIsLoading,
|
||||
query?.section,
|
||||
] );
|
||||
}, [ 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 (
|
||||
<Products
|
||||
products={ products }
|
||||
categorySelector={ true }
|
||||
type={ ProductType.extension }
|
||||
/>
|
||||
);
|
||||
case 'themes':
|
||||
return (
|
||||
<Products
|
||||
products={ products }
|
||||
categorySelector={ true }
|
||||
type={ ProductType.theme }
|
||||
/>
|
||||
);
|
||||
case 'business-services':
|
||||
return (
|
||||
<Products
|
||||
products={ products }
|
||||
products={ filteredProducts }
|
||||
categorySelector={ true }
|
||||
type={ ProductType.businessService }
|
||||
/>
|
||||
);
|
||||
case 'search':
|
||||
return (
|
||||
<SearchResults
|
||||
products={ products }
|
||||
type={ SearchResultType.all }
|
||||
type={ getProductType( selectedTab ) }
|
||||
/>
|
||||
);
|
||||
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 (
|
||||
<div className="woocommerce-marketplace__content">
|
||||
<Promotions />
|
||||
<InstallNewProductModal products={ products } />
|
||||
<InstallNewProductModal products={ filteredProducts } />
|
||||
{ selectedTab !== 'business-services' &&
|
||||
selectedTab !== 'my-subscriptions' && <ConnectNotice /> }
|
||||
{ selectedTab !== 'business-services' && <PluginInstallNotice /> }
|
||||
|
@ -197,11 +424,15 @@ export default function Content(): JSX.Element {
|
|||
{ selectedTab !== 'business-services' && (
|
||||
<SubscriptionsExpiredExpiringNotice type="expiring" />
|
||||
) }
|
||||
{ selectedTab !== 'business-services' && (
|
||||
<SubscriptionsExpiredExpiringNotice type="missing" />
|
||||
) }
|
||||
|
||||
{ renderContent() }
|
||||
{ ! isLoading && shouldShowLoadMoreButton() && (
|
||||
<LoadMoreButton
|
||||
onLoadMore={ loadMoreProducts }
|
||||
isBusy={ isLoadingMore }
|
||||
disabled={ isLoadingMore }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<Button
|
||||
className="woocommerce-marketplace__load-more"
|
||||
variant={ 'secondary' }
|
||||
onClick={ handleClick }
|
||||
isBusy={ isBusy }
|
||||
disabled={ disabled }
|
||||
>
|
||||
{ __( 'Load more', 'woocommerce' ) }
|
||||
</Button>
|
||||
);
|
||||
}
|
|
@ -191,6 +191,8 @@ function ProductCard( props: ProductCardProps ): JSX.Element {
|
|||
return (
|
||||
<Card
|
||||
className={ classNames }
|
||||
id={ `product-${ product.id }` }
|
||||
tabIndex={ -1 }
|
||||
aria-hidden={ isLoading }
|
||||
style={ inlineCss() }
|
||||
>
|
||||
|
|
|
@ -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 <></>;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
export type SearchAPIJSONType = {
|
||||
products: Array< SearchAPIProductType >;
|
||||
total_pages: number;
|
||||
total_products: number;
|
||||
};
|
||||
|
||||
export type SearchAPIProductType = {
|
||||
|
|
|
@ -9,10 +9,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.woocommerce-marketplace__sub-header {
|
||||
display: flex;
|
||||
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 {
|
||||
margin: 16px 0 6px auto;
|
||||
}
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 && (
|
||||
<CategorySelector type={ props.type } />
|
||||
) }
|
||||
<ProductLoader hasTitle={ false } type={ props.type } />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if ( products.length === 0 ) {
|
||||
let type = SearchResultType.all;
|
||||
|
||||
|
@ -154,28 +139,14 @@ export default function Products( props: ProductsProps ) {
|
|||
: ''
|
||||
);
|
||||
|
||||
if ( isLoading ) {
|
||||
return (
|
||||
<>
|
||||
{ props.categorySelector && (
|
||||
<CategorySelector type={ props.type } />
|
||||
) }
|
||||
<ProductLoader hasTitle={ false } type={ props.type } />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ containerClassName }>
|
||||
{ selectedTab === 'search' && (
|
||||
<h2 className={ productListTitleClassName }>
|
||||
{ isLoading ? ' ' : title }
|
||||
</h2>
|
||||
) }
|
||||
<div className="woocommerce-marketplace__sub-header">
|
||||
<nav className="woocommerce-marketplace__sub-header">
|
||||
<div className="woocommerce-marketplace__sub-header__categories">
|
||||
{ props.categorySelector && (
|
||||
<CategorySelector type={ props.type } />
|
||||
) }
|
||||
</div>
|
||||
{ props.type === 'theme' && (
|
||||
<Button
|
||||
className="woocommerce-marketplace__customize-your-store-button"
|
||||
|
@ -192,7 +163,7 @@ export default function Products( props: ProductsProps ) {
|
|||
} }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
</nav>
|
||||
{ isModalOpen && (
|
||||
<ThemeSwitchWarningModal
|
||||
setIsModalOpen={ setIsModalOpen }
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
@import "../../stylesheets/_variables.scss";
|
||||
|
||||
.woocommerce-marketplace__search-results {
|
||||
.woocommerce-marketplace {
|
||||
&__view-all-button {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
.woocommerce-marketplace__product-list-content--collapsed {
|
||||
.woocommerce-marketplace__product-card {
|
||||
&:nth-child(n+7) {
|
||||
display: none;
|
||||
@media screen and (min-width: $breakpoint-huge) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,193 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useQuery } from '@woocommerce/navigation';
|
||||
import { useContext } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './search-results.scss';
|
||||
import { Product, ProductType, SearchResultType } from '../product-list/types';
|
||||
import Products from '../products/products';
|
||||
import NoResults from '../product-list-content/no-results';
|
||||
import { MarketplaceContext } from '../../contexts/marketplace-context';
|
||||
import {
|
||||
MARKETPLACE_ITEMS_PER_PAGE,
|
||||
MARKETPLACE_SEARCH_RESULTS_PER_PAGE,
|
||||
} from '../../../marketplace/components/constants';
|
||||
|
||||
export interface SearchResultProps {
|
||||
products: Product[];
|
||||
type: SearchResultType;
|
||||
}
|
||||
|
||||
export default function SearchResults( props: SearchResultProps ): JSX.Element {
|
||||
const extensionList = props.products.filter(
|
||||
( product ) => 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 (
|
||||
<Products
|
||||
products={ products }
|
||||
type={ type }
|
||||
categorySelector={
|
||||
overrides.categorySelector ?? showCategorySelector
|
||||
}
|
||||
searchTerm={ searchTerm }
|
||||
showAllButton={ overrides.showAllButton ?? true }
|
||||
perPage={ overrides.perPage ?? MARKETPLACE_ITEMS_PER_PAGE }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<NoResults
|
||||
type={ SearchResultType.all }
|
||||
showHeading={ true }
|
||||
heading={
|
||||
canShowBusinessServices
|
||||
? __(
|
||||
'No extensions, themes or business services found…',
|
||||
'woocommerce'
|
||||
)
|
||||
: __(
|
||||
'No extensions or themes found…',
|
||||
'woocommerce'
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="woocommerce-marketplace__search-results">
|
||||
{ content() }
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
|
||||
.woocommerce-marketplace__search-button {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
.components-input-control__input {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<div className="woocommerce-marketplace__search">
|
||||
<label
|
||||
className="screen-reader-text"
|
||||
htmlFor="woocommerce-marketplace-search-query"
|
||||
>
|
||||
{ placeholder }
|
||||
</label>
|
||||
<input
|
||||
id="woocommerce-marketplace-search-query"
|
||||
<SearchControl
|
||||
label={ searchPlaceholder }
|
||||
placeholder={ searchPlaceholder }
|
||||
value={ searchTerm }
|
||||
className="woocommerce-marketplace__search-input"
|
||||
type="search"
|
||||
name="woocommerce-marketplace-search-query"
|
||||
placeholder={ placeholder }
|
||||
onChange={ handleInputChange }
|
||||
onChange={ setSearchTerm }
|
||||
onKeyUp={ handleKeyUp }
|
||||
className="woocommerce-marketplace__search"
|
||||
/>
|
||||
<button
|
||||
id="woocommerce-marketplace-search-button"
|
||||
className="woocommerce-marketplace__search-button"
|
||||
aria-label={ __( 'Search', 'woocommerce' ) }
|
||||
onClick={ runSearch }
|
||||
>
|
||||
<Icon icon={ search } size={ 32 } />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 && (
|
||||
<span className="woocommerce-marketplace__update-count">
|
||||
<span
|
||||
className={ clsx(
|
||||
'woocommerce-marketplace__update-count',
|
||||
`woocommerce-marketplace__update-count-${ tabKey }`,
|
||||
{
|
||||
'is-active': tabKey === selectedTab,
|
||||
}
|
||||
) }
|
||||
>
|
||||
<span> { tabs[ tabKey ]?.updateCount } </span>
|
||||
</span>
|
||||
) }
|
||||
|
@ -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 (
|
||||
<nav
|
||||
className={ clsx(
|
||||
|
@ -181,7 +201,7 @@ const Tabs = ( props: TabsProps ): JSX.Element => {
|
|||
additionalClassNames || []
|
||||
) }
|
||||
>
|
||||
{ renderTabs( marketplaceContextValue, visibleTabs ) }
|
||||
{ renderTabs( marketplaceContextValue, visibleTabs, tabs, query ) }
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: update
|
||||
|
||||
Update In-App Marketplace category selector
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Replace marketplace search component with SearchControl from @wordpress/components
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Fix the loading state for the In-App Marketplace search
|
|
@ -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.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Change the way search results are displayed in the in-app marketplace
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add search result counts to the in-app marketplace header tabs (Extensions area)
|
Loading…
Reference in New Issue