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";
|
@import "../../stylesheets/_variables.scss";
|
||||||
|
|
||||||
.woocommerce-marketplace__category-selector {
|
.woocommerce-marketplace__category-selector {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
margin: $grid-unit-20 0 0 0;
|
margin: 0;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-marketplace__category-item {
|
.woocommerce-marketplace__category-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
.components-dropdown {
|
.components-dropdown {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -50,7 +54,6 @@
|
||||||
|
|
||||||
.woocommerce-marketplace__category-selector--full-width {
|
.woocommerce-marketplace__category-selector--full-width {
|
||||||
display: none;
|
display: none;
|
||||||
margin-top: $grid-unit-15;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: $break-medium) {
|
@media screen and (max-width: $break-medium) {
|
||||||
|
@ -122,3 +125,22 @@
|
||||||
background-color: $gray-900;
|
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
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { useState, useEffect } from '@wordpress/element';
|
import { useState, useEffect, useRef } from '@wordpress/element';
|
||||||
import { __ } from '@wordpress/i18n';
|
|
||||||
import { useQuery } from '@woocommerce/navigation';
|
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
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import CategoryLink from './category-link';
|
import CategoryLink from './category-link';
|
||||||
import CategoryDropdown from './category-dropdown';
|
|
||||||
import { Category, CategoryAPIItem } from './types';
|
import { Category, CategoryAPIItem } from './types';
|
||||||
import { fetchCategories } from '../../utils/functions';
|
import { fetchCategories } from '../../utils/functions';
|
||||||
import './category-selector.scss';
|
|
||||||
import { ProductType } from '../product-list/types';
|
import { ProductType } from '../product-list/types';
|
||||||
|
import CategoryDropdown from './category-dropdown';
|
||||||
|
import './category-selector.scss';
|
||||||
|
|
||||||
const ALL_CATEGORIES_SLUGS = {
|
const ALL_CATEGORIES_SLUGS = {
|
||||||
[ ProductType.extension ]: '_all',
|
[ ProductType.extension ]: '_all',
|
||||||
|
@ -29,32 +30,21 @@ interface CategorySelectorProps {
|
||||||
export default function CategorySelector(
|
export default function CategorySelector(
|
||||||
props: CategorySelectorProps
|
props: CategorySelectorProps
|
||||||
): JSX.Element {
|
): JSX.Element {
|
||||||
const [ visibleItems, setVisibleItems ] = useState< Category[] >( [] );
|
|
||||||
const [ dropdownItems, setDropdownItems ] = useState< Category[] >( [] );
|
|
||||||
const [ selected, setSelected ] = useState< Category >();
|
const [ selected, setSelected ] = useState< Category >();
|
||||||
const [ isLoading, setIsLoading ] = useState( false );
|
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();
|
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( () => {
|
useEffect( () => {
|
||||||
setIsLoading( true );
|
setIsLoading( true );
|
||||||
|
|
||||||
|
@ -72,21 +62,125 @@ export default function CategorySelector(
|
||||||
return category.slug !== '_featured';
|
return category.slug !== '_featured';
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// Split array into two from 7th item
|
setCategoriesToShow( categories );
|
||||||
const visibleCategoryItems = categories.slice( 0, 7 );
|
|
||||||
const dropdownCategoryItems = categories.slice( 7 );
|
|
||||||
|
|
||||||
setVisibleItems( visibleCategoryItems );
|
|
||||||
setDropdownItems( dropdownCategoryItems );
|
|
||||||
} )
|
} )
|
||||||
.catch( () => {
|
.catch( () => {
|
||||||
setVisibleItems( [] );
|
setCategoriesToShow( [] );
|
||||||
setDropdownItems( [] );
|
|
||||||
} )
|
} )
|
||||||
.finally( () => {
|
.finally( () => {
|
||||||
setIsLoading( false );
|
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() {
|
function mobileCategoryDropdownLabel() {
|
||||||
const allCategoriesText = __( 'All Categories', 'woocommerce' );
|
const allCategoriesText = __( 'All Categories', 'woocommerce' );
|
||||||
|
@ -102,16 +196,6 @@ export default function CategorySelector(
|
||||||
return selected.label;
|
return selected.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSelectedInDropdown() {
|
|
||||||
if ( ! selected ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dropdownItems.find(
|
|
||||||
( category ) => category.slug === selected.slug
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( isLoading ) {
|
if ( isLoading ) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -131,50 +215,62 @@ export default function CategorySelector(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ul className="woocommerce-marketplace__category-selector">
|
<ul
|
||||||
{ visibleItems.map( ( category ) => (
|
className="woocommerce-marketplace__category-selector"
|
||||||
|
aria-label="Categories"
|
||||||
|
ref={ categorySelectorRef }
|
||||||
|
>
|
||||||
|
{ categoriesToShow.map( ( category ) => (
|
||||||
<li
|
<li
|
||||||
className="woocommerce-marketplace__category-item"
|
className="woocommerce-marketplace__category-item"
|
||||||
key={ category.slug }
|
key={ category.slug }
|
||||||
|
ref={
|
||||||
|
category.slug === selected?.slug
|
||||||
|
? selectedCategoryRef
|
||||||
|
: null
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<CategoryLink
|
<CategoryLink
|
||||||
{ ...category }
|
{ ...category }
|
||||||
selected={ category.slug === selected?.slug }
|
selected={ category.slug === selected?.slug }
|
||||||
|
aria-current={ category.slug === selected?.slug }
|
||||||
/>
|
/>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
|
|
||||||
<div className="woocommerce-marketplace__category-selector--full-width">
|
<div className="woocommerce-marketplace__category-selector--full-width">
|
||||||
<CategoryDropdown
|
<CategoryDropdown
|
||||||
type={ props.type }
|
type={ props.type }
|
||||||
label={ mobileCategoryDropdownLabel() }
|
label={ mobileCategoryDropdownLabel() }
|
||||||
categories={ visibleItems.concat( dropdownItems ) }
|
categories={ categoriesToShow }
|
||||||
buttonClassName="woocommerce-marketplace__category-dropdown-button"
|
buttonClassName="woocommerce-marketplace__category-dropdown-button"
|
||||||
className="woocommerce-marketplace__category-dropdown"
|
className="woocommerce-marketplace__category-dropdown"
|
||||||
contentClassName="woocommerce-marketplace__category-dropdown-content"
|
contentClassName="woocommerce-marketplace__category-dropdown-content"
|
||||||
selected={ selected }
|
selected={ selected }
|
||||||
/>
|
/>
|
||||||
</div>
|
</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';
|
'/wp-json/wccom-extensions/1.0/search';
|
||||||
export const MARKETPLACE_CATEGORY_API_PATH =
|
export const MARKETPLACE_CATEGORY_API_PATH =
|
||||||
'/wp-json/wccom-extensions/1.0/categories';
|
'/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_SEARCH_RESULTS_PER_PAGE = 8;
|
||||||
export const MARKETPLACE_CART_PATH = MARKETPLACE_HOST + '/cart/';
|
export const MARKETPLACE_CART_PATH = MARKETPLACE_HOST + '/cart/';
|
||||||
export const MARKETPLACE_RENEW_SUBSCRIPTON_PATH =
|
export const MARKETPLACE_RENEW_SUBSCRIPTON_PATH =
|
||||||
|
|
|
@ -1,22 +1,29 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { useContext, useEffect, useState } from '@wordpress/element';
|
import {
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
} from '@wordpress/element';
|
||||||
import { useQuery } from '@woocommerce/navigation';
|
import { useQuery } from '@woocommerce/navigation';
|
||||||
|
import { speak } from '@wordpress/a11y';
|
||||||
|
import { __, sprintf } from '@wordpress/i18n';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import './content.scss';
|
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 { getAdminSetting } from '~/utils/admin-settings';
|
||||||
import Discover from '../discover/discover';
|
import Discover from '../discover/discover';
|
||||||
import Products from '../products/products';
|
import Products from '../products/products';
|
||||||
import SearchResults from '../search-results/search-results';
|
|
||||||
import MySubscriptions from '../my-subscriptions/my-subscriptions';
|
import MySubscriptions from '../my-subscriptions/my-subscriptions';
|
||||||
import { MarketplaceContext } from '../../contexts/marketplace-context';
|
import { MarketplaceContext } from '../../contexts/marketplace-context';
|
||||||
import { fetchSearchResults } from '../../utils/functions';
|
import { fetchSearchResults, getProductType } from '../../utils/functions';
|
||||||
import { SubscriptionsContextProvider } from '../../contexts/subscriptions-context';
|
import { SubscriptionsContextProvider } from '../../contexts/subscriptions-context';
|
||||||
|
import { SearchResultsCountType } from '../../contexts/types';
|
||||||
import {
|
import {
|
||||||
recordMarketplaceView,
|
recordMarketplaceView,
|
||||||
recordLegacyTabView,
|
recordLegacyTabView,
|
||||||
|
@ -26,149 +33,350 @@ import Promotions from '../promotions/promotions';
|
||||||
import ConnectNotice from '~/marketplace/components/connect-notice/connect-notice';
|
import ConnectNotice from '~/marketplace/components/connect-notice/connect-notice';
|
||||||
import PluginInstallNotice from '../woo-update-manager-plugin/plugin-install-notice';
|
import PluginInstallNotice from '../woo-update-manager-plugin/plugin-install-notice';
|
||||||
import SubscriptionsExpiredExpiringNotice from '~/marketplace/components/my-subscriptions/subscriptions-expired-expiring-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 {
|
export default function Content(): JSX.Element {
|
||||||
const marketplaceContextValue = useContext( MarketplaceContext );
|
const marketplaceContextValue = useContext( MarketplaceContext );
|
||||||
const [ products, setProducts ] = useState< Product[] >( [] );
|
const [ allProducts, setAllProducts ] = useState< Product[] >( [] );
|
||||||
const { setIsLoading, selectedTab, setHasBusinessServices } =
|
const [ filteredProducts, setFilteredProducts ] = useState< Product[] >(
|
||||||
marketplaceContextValue;
|
[]
|
||||||
|
);
|
||||||
|
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();
|
const query = useQuery();
|
||||||
|
|
||||||
// On initial load of the in-app marketplace, fetch extensions, themes and business services
|
const searchCompleteAnnouncement = ( count: number ): void => {
|
||||||
// and check if there are any business services available on WCCOM
|
speak(
|
||||||
useEffect( () => {
|
sprintf(
|
||||||
const categories = [ '', 'themes', 'business-services' ];
|
// translators: %d is the number of products found.
|
||||||
const abortControllers = categories.map( () => new AbortController() );
|
__( '%d products found', 'woocommerce' ),
|
||||||
|
count
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
categories.forEach( ( category: string, index ) => {
|
const tagProductsWithType = (
|
||||||
const params = new URLSearchParams();
|
products: Product[],
|
||||||
if ( category !== '' ) {
|
type: ProductType
|
||||||
params.append( 'category', category );
|
): Product[] => {
|
||||||
}
|
return products.map( ( product ) => ( {
|
||||||
|
...product,
|
||||||
|
type,
|
||||||
|
} ) );
|
||||||
|
};
|
||||||
|
|
||||||
const wccomSettings = getAdminSetting( 'wccomHelper', false );
|
const loadMoreProducts = useCallback( () => {
|
||||||
if ( wccomSettings.storeCountry ) {
|
setIsLoadingMore( true );
|
||||||
params.append( 'country', wccomSettings.storeCountry );
|
const params = new URLSearchParams();
|
||||||
}
|
|
||||||
|
|
||||||
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 abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
|
|
||||||
if (
|
if ( query.category && query.category !== '_all' ) {
|
||||||
query.tab === undefined ||
|
params.append( 'category', query.category );
|
||||||
( query.tab &&
|
|
||||||
[ '', 'discover', 'my-subscriptions' ].includes( query.tab ) )
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading( true );
|
if ( query.tab === 'themes' || query.tab === 'business-services' ) {
|
||||||
setProducts( [] );
|
params.append( 'category', query.tab );
|
||||||
|
}
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
if ( query.term ) {
|
if ( query.term ) {
|
||||||
params.append( 'term', 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 );
|
const wccomSettings = getAdminSetting( 'wccomHelper', false );
|
||||||
if ( wccomSettings.storeCountry ) {
|
if ( wccomSettings.storeCountry ) {
|
||||||
params.append( 'country', wccomSettings.storeCountry );
|
params.append( 'country', wccomSettings.storeCountry );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
params.append( 'page', ( currentPage + 1 ).toString() );
|
||||||
|
|
||||||
fetchSearchResults( params, abortController.signal )
|
fetchSearchResults( params, abortController.signal )
|
||||||
.then( ( productList ) => {
|
.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( () => {
|
.catch( () => {
|
||||||
setProducts( [] );
|
speak( __( 'Error loading more products', 'woocommerce' ) );
|
||||||
} )
|
} )
|
||||||
.finally( () => {
|
.finally( () => {
|
||||||
// we are recording both the new and legacy events here for now
|
setIsLoadingMore( false );
|
||||||
// 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 );
|
|
||||||
} );
|
} );
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
abortController.abort();
|
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.term,
|
||||||
query.category,
|
query.category,
|
||||||
query?.tab,
|
setHasBusinessServices,
|
||||||
setIsLoading,
|
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 => {
|
const renderContent = (): JSX.Element => {
|
||||||
switch ( selectedTab ) {
|
switch ( selectedTab ) {
|
||||||
case 'extensions':
|
case 'extensions':
|
||||||
return (
|
|
||||||
<Products
|
|
||||||
products={ products }
|
|
||||||
categorySelector={ true }
|
|
||||||
type={ ProductType.extension }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'themes':
|
case 'themes':
|
||||||
return (
|
|
||||||
<Products
|
|
||||||
products={ products }
|
|
||||||
categorySelector={ true }
|
|
||||||
type={ ProductType.theme }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'business-services':
|
case 'business-services':
|
||||||
return (
|
return (
|
||||||
<Products
|
<Products
|
||||||
products={ products }
|
products={ filteredProducts }
|
||||||
categorySelector={ true }
|
categorySelector={ true }
|
||||||
type={ ProductType.businessService }
|
type={ getProductType( selectedTab ) }
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'search':
|
|
||||||
return (
|
|
||||||
<SearchResults
|
|
||||||
products={ products }
|
|
||||||
type={ SearchResultType.all }
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'discover':
|
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 (
|
return (
|
||||||
<div className="woocommerce-marketplace__content">
|
<div className="woocommerce-marketplace__content">
|
||||||
<Promotions />
|
<Promotions />
|
||||||
<InstallNewProductModal products={ products } />
|
<InstallNewProductModal products={ filteredProducts } />
|
||||||
{ selectedTab !== 'business-services' &&
|
{ selectedTab !== 'business-services' &&
|
||||||
selectedTab !== 'my-subscriptions' && <ConnectNotice /> }
|
selectedTab !== 'my-subscriptions' && <ConnectNotice /> }
|
||||||
{ selectedTab !== 'business-services' && <PluginInstallNotice /> }
|
{ selectedTab !== 'business-services' && <PluginInstallNotice /> }
|
||||||
|
@ -197,11 +424,15 @@ export default function Content(): JSX.Element {
|
||||||
{ selectedTab !== 'business-services' && (
|
{ selectedTab !== 'business-services' && (
|
||||||
<SubscriptionsExpiredExpiringNotice type="expiring" />
|
<SubscriptionsExpiredExpiringNotice type="expiring" />
|
||||||
) }
|
) }
|
||||||
{ selectedTab !== 'business-services' && (
|
|
||||||
<SubscriptionsExpiredExpiringNotice type="missing" />
|
|
||||||
) }
|
|
||||||
|
|
||||||
{ renderContent() }
|
{ renderContent() }
|
||||||
|
{ ! isLoading && shouldShowLoadMoreButton() && (
|
||||||
|
<LoadMoreButton
|
||||||
|
onLoadMore={ loadMoreProducts }
|
||||||
|
isBusy={ isLoadingMore }
|
||||||
|
disabled={ isLoadingMore }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-bottom: 1px solid $gutenberg-gray-300;
|
border-bottom: 1px solid $gutenberg-gray-300;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
gap: $medium-gap;
|
||||||
grid-template: "mktpl-title mktpl-search mktpl-meta" 60px
|
grid-template: "mktpl-title mktpl-search mktpl-meta" 60px
|
||||||
"mktpl-tabs mktpl-tabs mktpl-tabs" auto / 1fr 320px 36px;
|
"mktpl-tabs mktpl-tabs mktpl-tabs" auto / 1fr 320px 36px;
|
||||||
padding: 0 $content-spacing-large;
|
padding: 0 $content-spacing-large;
|
||||||
|
@ -73,17 +74,3 @@
|
||||||
padding: 0 $content-spacing-small;
|
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 (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={ classNames }
|
className={ classNames }
|
||||||
|
id={ `product-${ product.id }` }
|
||||||
|
tabIndex={ -1 }
|
||||||
aria-hidden={ isLoading }
|
aria-hidden={ isLoading }
|
||||||
style={ inlineCss() }
|
style={ inlineCss() }
|
||||||
>
|
>
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { useEffect, useState } from '@wordpress/element';
|
import { useEffect, useState } from '@wordpress/element';
|
||||||
import { useQuery } from '@woocommerce/navigation';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -22,8 +21,6 @@ export default function NoResults( props: {
|
||||||
} ): JSX.Element {
|
} ): JSX.Element {
|
||||||
const [ productGroups, setProductGroups ] = useState< ProductGroup[] >();
|
const [ productGroups, setProductGroups ] = useState< ProductGroup[] >();
|
||||||
const [ isLoading, setIsLoading ] = useState( false );
|
const [ isLoading, setIsLoading ] = useState( false );
|
||||||
const query = useQuery();
|
|
||||||
const showCategorySelector = query.tab === 'search' && query.section;
|
|
||||||
const productGroupsForSearchType = {
|
const productGroupsForSearchType = {
|
||||||
[ SearchResultType.all ]: [
|
[ SearchResultType.all ]: [
|
||||||
'most-popular',
|
'most-popular',
|
||||||
|
@ -123,10 +120,6 @@ export default function NoResults( props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
function categorySelector() {
|
function categorySelector() {
|
||||||
if ( ! showCategorySelector ) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( props.type === SearchResultType.all ) {
|
if ( props.type === SearchResultType.all ) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
export type SearchAPIJSONType = {
|
export type SearchAPIJSONType = {
|
||||||
products: Array< SearchAPIProductType >;
|
products: Array< SearchAPIProductType >;
|
||||||
|
total_pages: number;
|
||||||
|
total_products: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SearchAPIProductType = {
|
export type SearchAPIProductType = {
|
||||||
|
|
|
@ -9,10 +9,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.woocommerce-marketplace__sub-header {
|
.woocommerce-marketplace__sub-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
.woocommerce-marketplace__customize-your-store-button {
|
justify-content: space-between;
|
||||||
margin: 16px 0 6px auto;
|
gap: 32px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.woocommerce-marketplace__sub-header__categories {
|
||||||
|
flex: 1;
|
||||||
|
overflow-x: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-marketplace__customize-your-store-button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import {
|
import {
|
||||||
createInterpolateElement,
|
createInterpolateElement,
|
||||||
useContext,
|
useContext,
|
||||||
|
@ -24,7 +24,6 @@ import ProductListContent from '../product-list-content/product-list-content';
|
||||||
import ProductLoader from '../product-loader/product-loader';
|
import ProductLoader from '../product-loader/product-loader';
|
||||||
import NoResults from '../product-list-content/no-results';
|
import NoResults from '../product-list-content/no-results';
|
||||||
import { Product, ProductType, SearchResultType } from '../product-list/types';
|
import { Product, ProductType, SearchResultType } from '../product-list/types';
|
||||||
import { MARKETPLACE_ITEMS_PER_PAGE } from '../constants';
|
|
||||||
import { ADMIN_URL } from '~/utils/admin-settings';
|
import { ADMIN_URL } from '~/utils/admin-settings';
|
||||||
import { ThemeSwitchWarningModal } from '~/customize-store/intro/warning-modals';
|
import { ThemeSwitchWarningModal } from '~/customize-store/intro/warning-modals';
|
||||||
|
|
||||||
|
@ -54,12 +53,10 @@ const LABELS = {
|
||||||
|
|
||||||
export default function Products( props: ProductsProps ) {
|
export default function Products( props: ProductsProps ) {
|
||||||
const marketplaceContextValue = useContext( MarketplaceContext );
|
const marketplaceContextValue = useContext( MarketplaceContext );
|
||||||
const { isLoading, selectedTab } = marketplaceContextValue;
|
const { isLoading } = marketplaceContextValue;
|
||||||
const label = LABELS[ props.type ].label;
|
const label = LABELS[ props.type ].label;
|
||||||
const singularLabel = LABELS[ props.type ].singularLabel;
|
|
||||||
const query = useQuery();
|
const query = useQuery();
|
||||||
const category = query?.category;
|
const category = query?.category;
|
||||||
const perPage = props.perPage ?? MARKETPLACE_ITEMS_PER_PAGE;
|
|
||||||
interface Theme {
|
interface Theme {
|
||||||
stylesheet?: string;
|
stylesheet?: string;
|
||||||
}
|
}
|
||||||
|
@ -94,42 +91,30 @@ export default function Products( props: ProductsProps ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the total number of products before we slice it later.
|
// Store the total number of products before we slice it later.
|
||||||
const productTotalCount = props.products?.length ?? 0;
|
const products = props.products ?? [];
|
||||||
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 labelForClassName =
|
const labelForClassName =
|
||||||
label === 'business services' ? 'business-services' : label;
|
label === 'business services' ? 'business-services' : label;
|
||||||
|
|
||||||
const baseContainerClass = 'woocommerce-marketplace__search-';
|
const baseContainerClass = 'woocommerce-marketplace__search-';
|
||||||
const baseProductListTitleClass = 'product-list-title--';
|
|
||||||
|
|
||||||
const containerClassName = clsx( baseContainerClass + labelForClassName );
|
const containerClassName = clsx( baseContainerClass + labelForClassName );
|
||||||
const productListTitleClassName = clsx(
|
|
||||||
'woocommerce-marketplace__product-list-title',
|
|
||||||
baseContainerClass + baseProductListTitleClass + labelForClassName,
|
|
||||||
{ 'is-loading': isLoading }
|
|
||||||
);
|
|
||||||
const viewAllButonClassName = clsx(
|
const viewAllButonClassName = clsx(
|
||||||
'woocommerce-marketplace__view-all-button',
|
'woocommerce-marketplace__view-all-button',
|
||||||
baseContainerClass + 'button-' + labelForClassName
|
baseContainerClass + 'button-' + labelForClassName
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ( isLoading ) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{ props.categorySelector && (
|
||||||
|
<CategorySelector type={ props.type } />
|
||||||
|
) }
|
||||||
|
<ProductLoader hasTitle={ false } type={ props.type } />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ( products.length === 0 ) {
|
if ( products.length === 0 ) {
|
||||||
let type = SearchResultType.all;
|
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 (
|
return (
|
||||||
<div className={ containerClassName }>
|
<div className={ containerClassName }>
|
||||||
{ selectedTab === 'search' && (
|
<nav className="woocommerce-marketplace__sub-header">
|
||||||
<h2 className={ productListTitleClassName }>
|
<div className="woocommerce-marketplace__sub-header__categories">
|
||||||
{ isLoading ? ' ' : title }
|
{ props.categorySelector && (
|
||||||
</h2>
|
<CategorySelector type={ props.type } />
|
||||||
) }
|
) }
|
||||||
<div className="woocommerce-marketplace__sub-header">
|
</div>
|
||||||
{ props.categorySelector && (
|
|
||||||
<CategorySelector type={ props.type } />
|
|
||||||
) }
|
|
||||||
{ props.type === 'theme' && (
|
{ props.type === 'theme' && (
|
||||||
<Button
|
<Button
|
||||||
className="woocommerce-marketplace__customize-your-store-button"
|
className="woocommerce-marketplace__customize-your-store-button"
|
||||||
|
@ -192,7 +163,7 @@ export default function Products( props: ProductsProps ) {
|
||||||
} }
|
} }
|
||||||
/>
|
/>
|
||||||
) }
|
) }
|
||||||
</div>
|
</nav>
|
||||||
{ isModalOpen && (
|
{ isModalOpen && (
|
||||||
<ThemeSwitchWarningModal
|
<ThemeSwitchWarningModal
|
||||||
setIsModalOpen={ setIsModalOpen }
|
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 {
|
.woocommerce-marketplace__search {
|
||||||
grid-area: mktpl-search;
|
grid-area: mktpl-search;
|
||||||
background: $gutenberg-gray-100;
|
margin-top: 15px;
|
||||||
border: 1.5px solid transparent;
|
width: 320px;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width <= $breakpoint-medium) {
|
@media (width <= $breakpoint-medium) {
|
||||||
margin: $grid-unit-20 $grid-unit-20 $grid-unit-10 $grid-unit-20;
|
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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,26 +2,20 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { Icon, search } from '@wordpress/icons';
|
import { useEffect, useState } from '@wordpress/element';
|
||||||
import { useContext, useEffect, useState } from '@wordpress/element';
|
|
||||||
import { navigateTo, getNewPath, useQuery } from '@woocommerce/navigation';
|
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
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import './search.scss';
|
import './search.scss';
|
||||||
import { MARKETPLACE_PATH } from '../constants';
|
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.
|
* Search component.
|
||||||
|
@ -30,14 +24,10 @@ const searchPlaceholderNoBusinessServices = __(
|
||||||
*/
|
*/
|
||||||
function Search(): JSX.Element {
|
function Search(): JSX.Element {
|
||||||
const [ searchTerm, setSearchTerm ] = useState( '' );
|
const [ searchTerm, setSearchTerm ] = useState( '' );
|
||||||
const { hasBusinessServices } = useContext( MarketplaceContext );
|
const searchPlaceholder = __( 'Search Marketplace', 'woocommerce' );
|
||||||
|
|
||||||
const query = useQuery();
|
const query = useQuery();
|
||||||
|
|
||||||
const placeholder = hasBusinessServices
|
|
||||||
? searchPlaceholder
|
|
||||||
: searchPlaceholderNoBusinessServices;
|
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
if ( query.term ) {
|
if ( query.term ) {
|
||||||
setSearchTerm( query.term );
|
setSearchTerm( query.term );
|
||||||
|
@ -46,21 +36,16 @@ function Search(): JSX.Element {
|
||||||
}
|
}
|
||||||
}, [ query.term ] );
|
}, [ query.term ] );
|
||||||
|
|
||||||
useEffect( () => {
|
|
||||||
if ( query.tab !== 'search' ) {
|
|
||||||
setSearchTerm( '' );
|
|
||||||
}
|
|
||||||
}, [ query.tab ] );
|
|
||||||
|
|
||||||
const runSearch = () => {
|
const runSearch = () => {
|
||||||
const term = searchTerm.trim();
|
const newQuery: { term?: string; tab?: string } = query;
|
||||||
|
|
||||||
const newQuery: { term?: string; tab?: string } = {};
|
// If we're on 'Discover' or 'My subscriptions' when a search is initiated, move to the extensions tab
|
||||||
if ( term !== '' ) {
|
if ( ! newQuery.tab || newQuery.tab === 'my-subscriptions' ) {
|
||||||
newQuery.term = term;
|
newQuery.tab = 'extensions';
|
||||||
newQuery.tab = 'search';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newQuery.term = searchTerm.trim();
|
||||||
|
|
||||||
// When the search term changes, we reset the query string on purpose.
|
// When the search term changes, we reset the query string on purpose.
|
||||||
navigateTo( {
|
navigateTo( {
|
||||||
url: getNewPath( newQuery, MARKETPLACE_PATH, {} ),
|
url: getNewPath( newQuery, MARKETPLACE_PATH, {} ),
|
||||||
|
@ -69,12 +54,6 @@ function Search(): JSX.Element {
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInputChange = (
|
|
||||||
event: React.ChangeEvent< HTMLInputElement >
|
|
||||||
) => {
|
|
||||||
setSearchTerm( event.target.value );
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyUp = ( event: { key: string } ) => {
|
const handleKeyUp = ( event: { key: string } ) => {
|
||||||
if ( event.key === 'Enter' ) {
|
if ( event.key === 'Enter' ) {
|
||||||
runSearch();
|
runSearch();
|
||||||
|
@ -86,32 +65,14 @@ function Search(): JSX.Element {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="woocommerce-marketplace__search">
|
<SearchControl
|
||||||
<label
|
label={ searchPlaceholder }
|
||||||
className="screen-reader-text"
|
placeholder={ searchPlaceholder }
|
||||||
htmlFor="woocommerce-marketplace-search-query"
|
value={ searchTerm }
|
||||||
>
|
onChange={ setSearchTerm }
|
||||||
{ placeholder }
|
onKeyUp={ handleKeyUp }
|
||||||
</label>
|
className="woocommerce-marketplace__search"
|
||||||
<input
|
/>
|
||||||
id="woocommerce-marketplace-search-query"
|
|
||||||
value={ searchTerm }
|
|
||||||
className="woocommerce-marketplace__search-input"
|
|
||||||
type="search"
|
|
||||||
name="woocommerce-marketplace-search-query"
|
|
||||||
placeholder={ placeholder }
|
|
||||||
onChange={ handleInputChange }
|
|
||||||
onKeyUp={ handleKeyUp }
|
|
||||||
/>
|
|
||||||
<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;
|
text-align: center;
|
||||||
z-index: 26;
|
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) {
|
@media (width <= $breakpoint-medium) {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
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 { Button } from '@wordpress/components';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { getNewPath, navigateTo, useQuery } from '@woocommerce/navigation';
|
import { getNewPath, navigateTo, useQuery } from '@woocommerce/navigation';
|
||||||
|
@ -35,63 +35,26 @@ interface Tabs {
|
||||||
const wccomSettings = getAdminSetting( 'wccomHelper', {} );
|
const wccomSettings = getAdminSetting( 'wccomHelper', {} );
|
||||||
const wooUpdateCount = wccomSettings?.wooUpdateCount ?? 0;
|
const wooUpdateCount = wccomSettings?.wooUpdateCount ?? 0;
|
||||||
|
|
||||||
const tabs: Tabs = {
|
const setUrlTabParam = ( tabKey: string, query: Record< string, string > ) => {
|
||||||
search: {
|
const term = query.term ? { term: query.term.trim() } : {};
|
||||||
name: 'search',
|
|
||||||
title: __( 'Search results', 'woocommerce' ),
|
|
||||||
showUpdateCount: false,
|
|
||||||
updateCount: 0,
|
|
||||||
},
|
|
||||||
discover: {
|
|
||||||
name: 'discover',
|
|
||||||
title: __( 'Discover', 'woocommerce' ),
|
|
||||||
showUpdateCount: false,
|
|
||||||
updateCount: 0,
|
|
||||||
},
|
|
||||||
extensions: {
|
|
||||||
name: 'extensions',
|
|
||||||
title: __( 'Extensions', 'woocommerce' ),
|
|
||||||
showUpdateCount: false,
|
|
||||||
updateCount: 0,
|
|
||||||
},
|
|
||||||
themes: {
|
|
||||||
name: 'themes',
|
|
||||||
title: __( 'Themes', 'woocommerce' ),
|
|
||||||
showUpdateCount: false,
|
|
||||||
updateCount: 0,
|
|
||||||
},
|
|
||||||
'business-services': {
|
|
||||||
name: 'business-services',
|
|
||||||
title: __( 'Business services', 'woocommerce' ),
|
|
||||||
showUpdateCount: false,
|
|
||||||
updateCount: 0,
|
|
||||||
},
|
|
||||||
'my-subscriptions': {
|
|
||||||
name: 'my-subscriptions',
|
|
||||||
title: __( 'My subscriptions', 'woocommerce' ),
|
|
||||||
showUpdateCount: true,
|
|
||||||
updateCount: wooUpdateCount,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const setUrlTabParam = ( tabKey: string ) => {
|
|
||||||
navigateTo( {
|
navigateTo( {
|
||||||
url: getNewPath(
|
url: getNewPath(
|
||||||
{ tab: tabKey === DEFAULT_TAB_KEY ? undefined : tabKey },
|
{ tab: tabKey === DEFAULT_TAB_KEY ? undefined : tabKey },
|
||||||
MARKETPLACE_PATH,
|
MARKETPLACE_PATH,
|
||||||
{}
|
term
|
||||||
),
|
),
|
||||||
} );
|
} );
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVisibleTabs = ( selectedTab: string, hasBusinessServices = false ) => {
|
const getVisibleTabs = (
|
||||||
|
selectedTab: string,
|
||||||
|
hasBusinessServices = false,
|
||||||
|
tabs: Tabs
|
||||||
|
) => {
|
||||||
if ( selectedTab === '' ) {
|
if ( selectedTab === '' ) {
|
||||||
return tabs;
|
return tabs;
|
||||||
}
|
}
|
||||||
const currentVisibleTabs = { ...tabs };
|
const currentVisibleTabs = { ...tabs };
|
||||||
if ( selectedTab !== 'search' ) {
|
|
||||||
delete currentVisibleTabs.search;
|
|
||||||
}
|
|
||||||
if ( ! hasBusinessServices ) {
|
if ( ! hasBusinessServices ) {
|
||||||
delete currentVisibleTabs[ 'business-services' ];
|
delete currentVisibleTabs[ 'business-services' ];
|
||||||
}
|
}
|
||||||
|
@ -101,7 +64,9 @@ const getVisibleTabs = ( selectedTab: string, hasBusinessServices = false ) => {
|
||||||
|
|
||||||
const renderTabs = (
|
const renderTabs = (
|
||||||
marketplaceContextValue: MarketplaceContextType,
|
marketplaceContextValue: MarketplaceContextType,
|
||||||
visibleTabs: Tabs
|
visibleTabs: Tabs,
|
||||||
|
tabs: Tabs,
|
||||||
|
query: Record< string, string >
|
||||||
) => {
|
) => {
|
||||||
const { selectedTab, setSelectedTab } = marketplaceContextValue;
|
const { selectedTab, setSelectedTab } = marketplaceContextValue;
|
||||||
|
|
||||||
|
@ -110,7 +75,7 @@ const renderTabs = (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSelectedTab( tabKey );
|
setSelectedTab( tabKey );
|
||||||
setUrlTabParam( tabKey );
|
setUrlTabParam( tabKey, query );
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabContent = [];
|
const tabContent = [];
|
||||||
|
@ -143,7 +108,15 @@ const renderTabs = (
|
||||||
{ tabs[ tabKey ]?.title }
|
{ tabs[ tabKey ]?.title }
|
||||||
{ tabs[ tabKey ]?.showUpdateCount &&
|
{ tabs[ tabKey ]?.showUpdateCount &&
|
||||||
tabs[ tabKey ]?.updateCount > 0 && (
|
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> { tabs[ tabKey ]?.updateCount } </span>
|
||||||
</span>
|
</span>
|
||||||
) }
|
) }
|
||||||
|
@ -157,23 +130,70 @@ const renderTabs = (
|
||||||
const Tabs = ( props: TabsProps ): JSX.Element => {
|
const Tabs = ( props: TabsProps ): JSX.Element => {
|
||||||
const { additionalClassNames } = props;
|
const { additionalClassNames } = props;
|
||||||
const marketplaceContextValue = useContext( MarketplaceContext );
|
const marketplaceContextValue = useContext( MarketplaceContext );
|
||||||
const { selectedTab, setSelectedTab, hasBusinessServices } =
|
const { selectedTab, isLoading, setSelectedTab, hasBusinessServices } =
|
||||||
marketplaceContextValue;
|
marketplaceContextValue;
|
||||||
const [ visibleTabs, setVisibleTabs ] = useState( getVisibleTabs( '' ) );
|
const { searchResultsCount } = marketplaceContextValue;
|
||||||
|
|
||||||
const query: Record< string, string > = useQuery();
|
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( () => {
|
useEffect( () => {
|
||||||
if ( query?.tab && tabs[ query.tab ] ) {
|
if ( query?.tab && tabs[ query.tab ] ) {
|
||||||
setSelectedTab( query.tab );
|
setSelectedTab( query.tab );
|
||||||
} else if ( Object.keys( query ).length > 0 ) {
|
} else if ( Object.keys( query ).length > 0 ) {
|
||||||
setSelectedTab( DEFAULT_TAB_KEY );
|
setSelectedTab( DEFAULT_TAB_KEY );
|
||||||
}
|
}
|
||||||
}, [ query, setSelectedTab ] );
|
}, [ query, setSelectedTab, tabs ] );
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
setVisibleTabs( getVisibleTabs( selectedTab, hasBusinessServices ) );
|
setVisibleTabs(
|
||||||
}, [ selectedTab, hasBusinessServices ] );
|
getVisibleTabs( selectedTab, hasBusinessServices, tabs )
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( selectedTab === 'business-services' && ! hasBusinessServices ) {
|
||||||
|
setUrlTabParam( 'extensions', query );
|
||||||
|
}
|
||||||
|
}, [ selectedTab, hasBusinessServices, query, tabs ] );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
className={ clsx(
|
className={ clsx(
|
||||||
|
@ -181,7 +201,7 @@ const Tabs = ( props: TabsProps ): JSX.Element => {
|
||||||
additionalClassNames || []
|
additionalClassNames || []
|
||||||
) }
|
) }
|
||||||
>
|
>
|
||||||
{ renderTabs( marketplaceContextValue, visibleTabs ) }
|
{ renderTabs( marketplaceContextValue, visibleTabs, tabs, query ) }
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { useState, useEffect, createContext } from '@wordpress/element';
|
import {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
createContext,
|
||||||
|
} from '@wordpress/element';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { MarketplaceContextType } from './types';
|
import { SearchResultsCountType, MarketplaceContextType } from './types';
|
||||||
import { getAdminSetting } from '../../utils/admin-settings';
|
import { getAdminSetting } from '../../utils/admin-settings';
|
||||||
|
|
||||||
export const MarketplaceContext = createContext< MarketplaceContextType >( {
|
export const MarketplaceContext = createContext< MarketplaceContextType >( {
|
||||||
|
@ -18,6 +23,12 @@ export const MarketplaceContext = createContext< MarketplaceContextType >( {
|
||||||
addInstalledProduct: () => {},
|
addInstalledProduct: () => {},
|
||||||
hasBusinessServices: false,
|
hasBusinessServices: false,
|
||||||
setHasBusinessServices: () => {},
|
setHasBusinessServices: () => {},
|
||||||
|
searchResultsCount: {
|
||||||
|
extensions: 0,
|
||||||
|
themes: 0,
|
||||||
|
'business-services': 0,
|
||||||
|
},
|
||||||
|
setSearchResultsCount: () => {},
|
||||||
} );
|
} );
|
||||||
|
|
||||||
export function MarketplaceContextProvider( props: {
|
export function MarketplaceContextProvider( props: {
|
||||||
|
@ -29,6 +40,22 @@ export function MarketplaceContextProvider( props: {
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
const [ hasBusinessServices, setHasBusinessServices ] = useState( false );
|
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
|
* Knowing installed products will help us to determine which products
|
||||||
|
@ -59,6 +86,8 @@ export function MarketplaceContextProvider( props: {
|
||||||
addInstalledProduct,
|
addInstalledProduct,
|
||||||
hasBusinessServices,
|
hasBusinessServices,
|
||||||
setHasBusinessServices,
|
setHasBusinessServices,
|
||||||
|
searchResultsCount,
|
||||||
|
setSearchResultsCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -8,6 +8,12 @@ import { Options } from '@wordpress/notices';
|
||||||
*/
|
*/
|
||||||
import { Subscription } from '../components/my-subscriptions/types';
|
import { Subscription } from '../components/my-subscriptions/types';
|
||||||
|
|
||||||
|
export interface SearchResultsCountType {
|
||||||
|
extensions: number;
|
||||||
|
themes: number;
|
||||||
|
'business-services': number;
|
||||||
|
}
|
||||||
|
|
||||||
export type MarketplaceContextType = {
|
export type MarketplaceContextType = {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
setIsLoading: ( isLoading: boolean ) => void;
|
setIsLoading: ( isLoading: boolean ) => void;
|
||||||
|
@ -17,6 +23,10 @@ export type MarketplaceContextType = {
|
||||||
addInstalledProduct: ( slug: string ) => void;
|
addInstalledProduct: ( slug: string ) => void;
|
||||||
hasBusinessServices: boolean;
|
hasBusinessServices: boolean;
|
||||||
setHasBusinessServices: ( hasBusinessServices: boolean ) => void;
|
setHasBusinessServices: ( hasBusinessServices: boolean ) => void;
|
||||||
|
searchResultsCount: SearchResultsCountType;
|
||||||
|
setSearchResultsCount: (
|
||||||
|
updatedCounts: Partial< SearchResultsCountType >
|
||||||
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SubscriptionsContextType = {
|
export type SubscriptionsContextType = {
|
||||||
|
|
|
@ -107,7 +107,11 @@ async function fetchJsonWithCache(
|
||||||
async function fetchSearchResults(
|
async function fetchSearchResults(
|
||||||
params: URLSearchParams,
|
params: URLSearchParams,
|
||||||
abortSignal?: AbortSignal
|
abortSignal?: AbortSignal
|
||||||
): Promise< Product[] > {
|
): Promise< {
|
||||||
|
products: Product[];
|
||||||
|
totalPages: number;
|
||||||
|
totalProducts: number;
|
||||||
|
} > {
|
||||||
const url =
|
const url =
|
||||||
MARKETPLACE_HOST +
|
MARKETPLACE_HOST +
|
||||||
MARKETPLACE_SEARCH_API_PATH +
|
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[] > {
|
function fetchCategories( type: ProductType ): Promise< CategoryAPIItem[] > {
|
||||||
const url = new URL( MARKETPLACE_HOST + MARKETPLACE_CATEGORY_API_PATH );
|
const url = new URL( MARKETPLACE_HOST + MARKETPLACE_CATEGORY_API_PATH );
|
||||||
|
|
||||||
|
@ -478,6 +496,7 @@ export {
|
||||||
fetchCategories,
|
fetchCategories,
|
||||||
fetchDiscoverPageData,
|
fetchDiscoverPageData,
|
||||||
fetchSearchResults,
|
fetchSearchResults,
|
||||||
|
getProductType,
|
||||||
fetchSubscriptions,
|
fetchSubscriptions,
|
||||||
refreshSubscriptions,
|
refreshSubscriptions,
|
||||||
getInstallUrl,
|
getInstallUrl,
|
||||||
|
|
|
@ -42,11 +42,6 @@ function recordMarketplaceView( props: MarketplaceViewProps ) {
|
||||||
eventProps.category = '_all';
|
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 );
|
recordEvent( 'marketplace_view', eventProps );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,11 +75,6 @@ function recordLegacyTabView( props: MarketplaceViewProps ) {
|
||||||
case 'themes':
|
case 'themes':
|
||||||
oldEventProps.section = 'themes';
|
oldEventProps.section = 'themes';
|
||||||
break;
|
break;
|
||||||
case 'search':
|
|
||||||
oldEventName = 'extensions_view_search';
|
|
||||||
oldEventProps.section = view;
|
|
||||||
oldEventProps.search_term = search_term || '';
|
|
||||||
break;
|
|
||||||
case 'my-subscriptions':
|
case 'my-subscriptions':
|
||||||
oldEventName = 'subscriptions_view';
|
oldEventName = 'subscriptions_view';
|
||||||
oldEventProps.section = 'helper';
|
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