Merge branch 'trunk' into update/wccom-18825-price-fields
This commit is contained in:
commit
49f5ccd1c5
|
@ -1,35 +1,30 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
# '1' is branch
|
# The hook documentation: https://git-scm.com/docs/githooks.html#_post_checkout
|
||||||
CHECKOUT_TYPE=$3
|
CHECKOUT_TYPE=$3
|
||||||
redColoured='\033[0;31m'
|
HEAD_NEW=$2
|
||||||
|
HEAD_PREVIOUS=$1
|
||||||
|
|
||||||
whiteColoured='\033[0m'
|
whiteColoured='\033[0m'
|
||||||
|
orangeColoured='\033[1;33m'
|
||||||
|
|
||||||
|
# '1' is a branch checkout
|
||||||
if [ "$CHECKOUT_TYPE" = '1' ]; then
|
if [ "$CHECKOUT_TYPE" = '1' ]; then
|
||||||
canUpdateDependencies='no'
|
|
||||||
|
|
||||||
# Prompt about pnpm versions mismatch when switching between branches.
|
# Prompt about pnpm versions mismatch when switching between branches.
|
||||||
currentPnpmVersion=$( ( command -v pnpm > /dev/null && pnpm -v ) || echo 'n/a' )
|
currentPnpmVersion=$( ( command -v pnpm > /dev/null && pnpm -v 2>/dev/null ) || echo 'n/a' )
|
||||||
targetPnpmVersion=$( grep packageManager package.json | sed -nr 's/.+packageManager.+pnpm@([[:digit:].]+).+/\1/p' )
|
targetPnpmVersion=$( grep packageManager package.json | sed -nr 's/.+packageManager.+pnpm@([[:digit:].]+).+/\1/p' )
|
||||||
if [ "$currentPnpmVersion" != "$targetPnpmVersion" ]; then
|
if [ "$currentPnpmVersion" != "$targetPnpmVersion" ]; then
|
||||||
printf "${redColoured}pnpm versions mismatch: in use '$currentPnpmVersion', needed '$targetPnpmVersion'. Here some hints how to solve this:\n"
|
printf "${orangeColoured}pnpm versions mismatch: in use '$currentPnpmVersion', needed '$targetPnpmVersion'. If you are working on something in this branch, here are some hints on how to solve this:\n"
|
||||||
printf "${redColoured}* actualize environment: 'nvm use && pnpm -v' (the most common case)\n"
|
printf "${orangeColoured}* actualize environment: 'nvm use && pnpm -v' (the most common case)\n"
|
||||||
printf "${redColoured}* install: 'npm install -g pnpm@$targetPnpmVersion'\n"
|
printf "${orangeColoured}* install: 'npm install -g pnpm@$targetPnpmVersion'\n"
|
||||||
else
|
|
||||||
canUpdateDependencies='yes'
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Auto-refresh dependencies when switching between branches.
|
# Auto-refresh dependencies when switching between branches.
|
||||||
changedManifests=$( ( git diff --name-only HEAD ORIG_HEAD | grep -E '(package.json|pnpm-lock.yaml|pnpm-workspace.yaml|composer.json|composer.lock)$' ) || echo '' )
|
changedManifests=$( ( git diff --name-only $HEAD_NEW $HEAD_PREVIOUS | grep -E '(package.json|pnpm-lock.yaml|pnpm-workspace.yaml|composer.json|composer.lock)$' ) || echo '' )
|
||||||
if [ -n "$changedManifests" ]; then
|
if [ -n "$changedManifests" ]; then
|
||||||
printf "${whiteColoured}It was a change in the following file(s) - refreshing dependencies:\n"
|
printf "${whiteColoured}The following file(s) in the new branch differs from the original one, dependencies might need to be refreshed:\n"
|
||||||
printf "${whiteColoured} %s\n" $changedManifests
|
printf "${whiteColoured} %s\n" $changedManifests
|
||||||
|
printf "${orangeColoured}If you are working on something in this branch, ensure to refresh dependencies with 'pnpm install --frozen-lockfile'\n"
|
||||||
if [ "$canUpdateDependencies" = 'yes' ]; then
|
|
||||||
pnpm install --frozen-lockfile
|
|
||||||
else
|
|
||||||
printf "${redColoured}Skipping dependencies refresh. Please actualize pnpm version and execute 'pnpm install --frozen-lockfile' manually.\n"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
# The hook documentation: https://git-scm.com/docs/githooks.html#_post_merge
|
||||||
|
|
||||||
changedManifests=$( ( git diff --name-only HEAD ORIG_HEAD | grep -E '(package.json|pnpm-lock.yaml|pnpm-workspace.yaml|composer.json|composer.lock)$' ) || echo '' )
|
changedManifests=$( ( git diff --name-only HEAD ORIG_HEAD | grep -E '(package.json|pnpm-lock.yaml|pnpm-workspace.yaml|composer.json|composer.lock)$' ) || echo '' )
|
||||||
if [ -n "$changedManifests" ]; then
|
if [ -n "$changedManifests" ]; then
|
||||||
printf "It was a change in the following file(s) - refreshing dependencies:\n"
|
printf "It was a change in the following file(s) - refreshing dependencies:\n"
|
||||||
|
|
|
@ -254,4 +254,4 @@ Displaying the variation in the front store works a bit differently for variable
|
||||||
|
|
||||||
## How to find hooks?
|
## How to find hooks?
|
||||||
|
|
||||||
Everyone will have their own preferred way, but for me, the quickest way is to look in the WooCommere plugin code. The code for each data section can be found in `/woocommerce/includes/admin/meta-boxes/views`. To view how the inventory section is handled check the `html-product-data-inventory.php` file, and for variations take a look at `html-variation-admin.php`.
|
Everyone will have their own preferred way, but for me, the quickest way is to look in the WooCommerce plugin code. The code for each data section can be found in `/woocommerce/includes/admin/meta-boxes/views`. To view how the inventory section is handled check the `html-product-data-inventory.php` file, and for variations take a look at `html-variation-admin.php`.
|
||||||
|
|
|
@ -23,6 +23,7 @@ import { GlobalStylesRenderer } from '@wordpress/edit-site/build-module/componen
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
|
import { trackEvent } from '../tracking';
|
||||||
import { editorIsLoaded } from '../utils';
|
import { editorIsLoaded } from '../utils';
|
||||||
import { BlockEditorContainer } from './block-editor-container';
|
import { BlockEditorContainer } from './block-editor-container';
|
||||||
|
|
||||||
|
@ -63,6 +64,7 @@ export const Editor = ( { isLoading }: { isLoading: boolean } ) => {
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
if ( ! isLoading ) {
|
if ( ! isLoading ) {
|
||||||
editorIsLoaded();
|
editorIsLoaded();
|
||||||
|
trackEvent( 'customize_your_store_assembler_hub_editor_loaded' );
|
||||||
}
|
}
|
||||||
}, [ isLoading ] );
|
}, [ isLoading ] );
|
||||||
|
|
||||||
|
|
|
@ -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,79 +33,157 @@ 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 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 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
return () => {
|
|
||||||
abortControllers.forEach( ( controller ) => {
|
|
||||||
controller.abort();
|
|
||||||
} );
|
|
||||||
};
|
};
|
||||||
} );
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [] );
|
|
||||||
|
|
||||||
// Get the content for this screen
|
const tagProductsWithType = (
|
||||||
useEffect( () => {
|
products: Product[],
|
||||||
|
type: ProductType
|
||||||
|
): Product[] => {
|
||||||
|
return products.map( ( product ) => ( {
|
||||||
|
...product,
|
||||||
|
type,
|
||||||
|
} ) );
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadMoreProducts = useCallback( () => {
|
||||||
|
setIsLoadingMore( true );
|
||||||
|
const params = new URLSearchParams();
|
||||||
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 ) {
|
const wccomSettings = getAdminSetting( 'wccomHelper', false );
|
||||||
params.append(
|
if ( wccomSettings.storeCountry ) {
|
||||||
'category',
|
params.append( 'country', wccomSettings.storeCountry );
|
||||||
query.category === '_all' ? '' : query.category
|
}
|
||||||
|
|
||||||
|
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' );
|
if ( newProducts.length > 0 ) {
|
||||||
} else if ( query?.tab === 'business-services' ) {
|
setFirstNewProductId( newProducts[ 0 ].id ?? 0 );
|
||||||
params.append( 'category', 'business-services' );
|
}
|
||||||
} else if ( query?.tab === 'search' ) {
|
|
||||||
params.append( 'category', 'extensions-themes-business-services' );
|
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 );
|
const wccomSettings = getAdminSetting( 'wccomHelper', false );
|
||||||
|
@ -106,69 +191,192 @@ export default function Content(): JSX.Element {
|
||||||
params.append( 'country', wccomSettings.storeCountry );
|
params.append( 'country', wccomSettings.storeCountry );
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchSearchResults( params, abortController.signal )
|
fetchSearchResults( params, abortControllers[ 0 ].signal )
|
||||||
.then( ( productList ) => {
|
.then( ( productList ) => {
|
||||||
setProducts( productList );
|
setAllProducts( productList.products );
|
||||||
|
setTotalPagesCategory( productList.totalPages );
|
||||||
|
setSearchResultsCount( {
|
||||||
|
[ query.tab ]: productList.totalProducts,
|
||||||
|
} );
|
||||||
|
|
||||||
|
searchCompleteAnnouncement( productList.totalProducts );
|
||||||
} )
|
} )
|
||||||
.catch( () => {
|
.catch( () => {
|
||||||
setProducts( [] );
|
setAllProducts( [] );
|
||||||
} )
|
} )
|
||||||
.finally( () => {
|
.finally( () => {
|
||||||
// we are recording both the new and legacy events here for now
|
setIsLoading( false );
|
||||||
// they're separate methods to make it easier to remove the legacy one later
|
} );
|
||||||
|
} 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 = {
|
const marketplaceViewProps = {
|
||||||
view: query?.tab,
|
view: query?.tab,
|
||||||
search_term: query?.term,
|
search_term: query?.term,
|
||||||
product_type: query?.section,
|
product_type: query?.section,
|
||||||
category: query?.category,
|
category: query?.category,
|
||||||
};
|
};
|
||||||
|
|
||||||
recordMarketplaceView( marketplaceViewProps );
|
recordMarketplaceView( marketplaceViewProps );
|
||||||
recordLegacyTabView( marketplaceViewProps );
|
recordLegacyTabView( marketplaceViewProps );
|
||||||
setIsLoading( false );
|
}, [ query?.tab, query?.term, query?.section, query?.category ] );
|
||||||
} );
|
|
||||||
return () => {
|
// Reset current page when tab, term, or category changes
|
||||||
abortController.abort();
|
useEffect( () => {
|
||||||
};
|
setCurrentPage( 1 );
|
||||||
}, [
|
setFirstNewProductId( 0 );
|
||||||
query.term,
|
}, [ selectedTab, query?.category, query?.term ] );
|
||||||
query.category,
|
|
||||||
query?.tab,
|
// Maintain product focus for accessibility
|
||||||
setIsLoading,
|
useEffect( () => {
|
||||||
query?.section,
|
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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -193,6 +193,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 }
|
|
||||||
</h2>
|
|
||||||
) }
|
|
||||||
<div className="woocommerce-marketplace__sub-header">
|
|
||||||
{ props.categorySelector && (
|
{ props.categorySelector && (
|
||||||
<CategorySelector type={ props.type } />
|
<CategorySelector type={ props.type } />
|
||||||
) }
|
) }
|
||||||
|
</div>
|
||||||
{ 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"
|
|
||||||
>
|
|
||||||
{ placeholder }
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="woocommerce-marketplace-search-query"
|
|
||||||
value={ searchTerm }
|
value={ searchTerm }
|
||||||
className="woocommerce-marketplace__search-input"
|
onChange={ setSearchTerm }
|
||||||
type="search"
|
|
||||||
name="woocommerce-marketplace-search-query"
|
|
||||||
placeholder={ placeholder }
|
|
||||||
onChange={ handleInputChange }
|
|
||||||
onKeyUp={ handleKeyUp }
|
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;
|
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 +
|
||||||
|
@ -153,9 +157,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 );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,6 +183,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 );
|
||||||
|
|
||||||
|
@ -482,6 +500,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: patch
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Expand the e2e suite we're running on WPCOM part #3.
|
|
@ -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.
|
|
@ -1,4 +0,0 @@
|
||||||
Significance: patch
|
|
||||||
Type: fix
|
|
||||||
|
|
||||||
Fix bug where manually triggering `added_to_cart` event without a button element caused an Exception.
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: dev
|
||||||
|
|
||||||
|
Track customize_your_store_assembler_hub_editor_loaded event to measure CYS loading time
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Wrap parse_str under a check to resolve deprecation notice
|
|
@ -0,0 +1,5 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: dev
|
||||||
|
Comment: Update unit test to account for WordPress nightly change. See core trac ticket 61739
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
@ -125,6 +125,7 @@ class WC_Product_CSV_Importer_Controller {
|
||||||
|
|
||||||
// Check that file is within an allowed location.
|
// Check that file is within an allowed location.
|
||||||
if ( $is_valid_file ) {
|
if ( $is_valid_file ) {
|
||||||
|
$normalized_path = wp_normalize_path( $path );
|
||||||
$in_valid_location = false;
|
$in_valid_location = false;
|
||||||
$valid_locations = array();
|
$valid_locations = array();
|
||||||
$valid_locations[] = ABSPATH;
|
$valid_locations[] = ABSPATH;
|
||||||
|
@ -135,7 +136,8 @@ class WC_Product_CSV_Importer_Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ( $valid_locations as $valid_location ) {
|
foreach ( $valid_locations as $valid_location ) {
|
||||||
if ( 0 === stripos( $path, trailingslashit( realpath( $valid_location ) ) ) ) {
|
$normalized_location = wp_normalize_path( realpath( $valid_location ) );
|
||||||
|
if ( 0 === stripos( $normalized_path, trailingslashit( $normalized_location ) ) ) {
|
||||||
$in_valid_location = true;
|
$in_valid_location = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -658,18 +658,18 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
|
||||||
/**
|
/**
|
||||||
* Action to signal that the value of 'stock_quantity' for a variation is about to change.
|
* Action to signal that the value of 'stock_quantity' for a variation is about to change.
|
||||||
*
|
*
|
||||||
* @param WC_Product $product The variation whose stock is about to change.
|
|
||||||
*
|
|
||||||
* @since 4.9
|
* @since 4.9
|
||||||
|
*
|
||||||
|
* @param int $product The variation whose stock is about to change.
|
||||||
*/
|
*/
|
||||||
do_action( 'woocommerce_variation_before_set_stock', $product );
|
do_action( 'woocommerce_variation_before_set_stock', $product );
|
||||||
} else {
|
} else {
|
||||||
/**
|
/**
|
||||||
* Action to signal that the value of 'stock_quantity' for a product is about to change.
|
* Action to signal that the value of 'stock_quantity' for a product is about to change.
|
||||||
*
|
*
|
||||||
* @param WC_Product $product The product whose stock is about to change.
|
|
||||||
*
|
|
||||||
* @since 4.9
|
* @since 4.9
|
||||||
|
*
|
||||||
|
* @param int $product The product whose stock is about to change.
|
||||||
*/
|
*/
|
||||||
do_action( 'woocommerce_product_before_set_stock', $product );
|
do_action( 'woocommerce_product_before_set_stock', $product );
|
||||||
}
|
}
|
||||||
|
|
|
@ -243,9 +243,30 @@ function wc_trigger_stock_change_notifications( $order, $changes ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
$order_notes = array();
|
$order_notes = array();
|
||||||
|
$no_stock_amount = absint( get_option( 'woocommerce_notify_no_stock_amount', 0 ) );
|
||||||
|
|
||||||
foreach ( $changes as $change ) {
|
foreach ( $changes as $change ) {
|
||||||
$order_notes[] = $change['product']->get_formatted_name() . ' ' . $change['from'] . '→' . $change['to'];
|
$order_notes[] = $change['product']->get_formatted_name() . ' ' . $change['from'] . '→' . $change['to'];
|
||||||
|
$low_stock_amount = absint( wc_get_low_stock_amount( wc_get_product( $change['product']->get_id() ) ) );
|
||||||
|
if ( $change['to'] <= $no_stock_amount ) {
|
||||||
|
/**
|
||||||
|
* Action to signal that the value of 'stock_quantity' for a variation is about to change.
|
||||||
|
*
|
||||||
|
* @since 4.9
|
||||||
|
*
|
||||||
|
* @param int $product The variation whose stock is about to change.
|
||||||
|
*/
|
||||||
|
do_action( 'woocommerce_no_stock', wc_get_product( $change['product']->get_id() ) );
|
||||||
|
} elseif ( $change['to'] <= $low_stock_amount ) {
|
||||||
|
/**
|
||||||
|
* Action to signal that the value of 'stock_quantity' for a product is about to change.
|
||||||
|
*
|
||||||
|
* @since 4.9
|
||||||
|
*
|
||||||
|
* @param int $product The product whose stock is about to change.
|
||||||
|
*/
|
||||||
|
do_action( 'woocommerce_low_stock', wc_get_product( $change['product']->get_id() ) );
|
||||||
|
}
|
||||||
|
|
||||||
if ( $change['to'] < 0 ) {
|
if ( $change['to'] < 0 ) {
|
||||||
/**
|
/**
|
||||||
|
@ -312,8 +333,6 @@ function wc_trigger_stock_change_actions( $product ) {
|
||||||
do_action( 'woocommerce_low_stock', $product );
|
do_action( 'woocommerce_low_stock', $product );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
add_action( 'woocommerce_variation_set_stock', 'wc_trigger_stock_change_actions' );
|
|
||||||
add_action( 'woocommerce_product_set_stock', 'wc_trigger_stock_change_actions' );
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Increase stock levels for items within an order.
|
* Increase stock levels for items within an order.
|
||||||
|
@ -485,11 +504,8 @@ function wc_get_low_stock_amount( WC_Product $product ) {
|
||||||
$low_stock_amount = $product->get_low_stock_amount();
|
$low_stock_amount = $product->get_low_stock_amount();
|
||||||
|
|
||||||
if ( '' === $low_stock_amount && $product->is_type( 'variation' ) ) {
|
if ( '' === $low_stock_amount && $product->is_type( 'variation' ) ) {
|
||||||
$parent_product = wc_get_product( $product->get_parent_id() );
|
$product = wc_get_product( $product->get_parent_id() );
|
||||||
|
$low_stock_amount = $product->get_low_stock_amount();
|
||||||
if ( $parent_product instanceof WC_Product ) {
|
|
||||||
$low_stock_amount = $parent_product->get_low_stock_amount();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( '' === $low_stock_amount ) {
|
if ( '' === $low_stock_amount ) {
|
||||||
|
|
|
@ -154,13 +154,15 @@ class WCAdminHelper {
|
||||||
'post_type' => 'product',
|
'post_type' => 'product',
|
||||||
);
|
);
|
||||||
|
|
||||||
parse_str( wp_parse_url( $url, PHP_URL_QUERY ), $url_params );
|
$query_string = wp_parse_url( $url, PHP_URL_QUERY );
|
||||||
|
if ( $query_string ) {
|
||||||
|
parse_str( $query_string, $url_params );
|
||||||
foreach ( $params as $key => $param ) {
|
foreach ( $params as $key => $param ) {
|
||||||
if ( isset( $url_params[ $key ] ) && $url_params[ $key ] === $param ) {
|
if ( isset( $url_params[ $key ] ) && $url_params[ $key ] === $param ) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WC store pages.
|
// WC store pages.
|
||||||
$store_pages = array(
|
$store_pages = array(
|
||||||
|
|
|
@ -23,6 +23,16 @@ config = {
|
||||||
'**/merchant/create-order.spec.js',
|
'**/merchant/create-order.spec.js',
|
||||||
'**/merchant/create-page.spec.js',
|
'**/merchant/create-page.spec.js',
|
||||||
'**/merchant/create-post.spec.js',
|
'**/merchant/create-post.spec.js',
|
||||||
|
'**/merchant/create-restricted-coupons.spec.js',
|
||||||
|
'**/merchant/create-shipping-classes.spec.js',
|
||||||
|
'**/merchant/create-shipping-zones.spec.js',
|
||||||
|
'**/merchant/create-woocommerce-blocks.spec.js',
|
||||||
|
'**/merchant/create-woocommerce-patterns.spec.js',
|
||||||
|
'**/merchant/customer-list.spec.js',
|
||||||
|
'**/merchant/customer-payment-page.spec.js',
|
||||||
|
'**/merchant/launch-your-store.spec.js',
|
||||||
|
'**/merchant/lost-password.spec.js',
|
||||||
|
'**/merchant/order-bulk-edit.spec.js',
|
||||||
],
|
],
|
||||||
grepInvert: /@skip-on-default-wpcom/,
|
grepInvert: /@skip-on-default-wpcom/,
|
||||||
},
|
},
|
||||||
|
|
|
@ -103,7 +103,10 @@ const test = baseTest.extend( {
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
||||||
test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => {
|
test.describe(
|
||||||
|
'Restricted coupon management',
|
||||||
|
{ tag: [ '@services', '@skip-on-default-wpcom' ] },
|
||||||
|
() => {
|
||||||
for ( const couponType of Object.keys( couponData ) ) {
|
for ( const couponType of Object.keys( couponData ) ) {
|
||||||
test( `can create new ${ couponType } coupon`, async ( {
|
test( `can create new ${ couponType } coupon`, async ( {
|
||||||
page,
|
page,
|
||||||
|
@ -124,7 +127,9 @@ test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => {
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder( '0' )
|
.getByPlaceholder( '0' )
|
||||||
.fill( couponData[ couponType ].amount );
|
.fill( couponData[ couponType ].amount );
|
||||||
await expect( page.getByText( 'Move to Trash' ) ).toBeVisible();
|
await expect(
|
||||||
|
page.getByText( 'Move to Trash' )
|
||||||
|
).toBeVisible();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// set up the restrictions for each coupon type
|
// set up the restrictions for each coupon type
|
||||||
|
@ -213,7 +218,10 @@ test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => {
|
||||||
const skipBrandsTests = true;
|
const skipBrandsTests = true;
|
||||||
|
|
||||||
// set exclude product brands
|
// set exclude product brands
|
||||||
if ( couponType === 'excludeProductBrands' && ! skipBrandsTests ) {
|
if (
|
||||||
|
couponType === 'excludeProductBrands' &&
|
||||||
|
! skipBrandsTests
|
||||||
|
) {
|
||||||
await test.step( 'set exclude product brands coupon', async () => {
|
await test.step( 'set exclude product brands coupon', async () => {
|
||||||
await page
|
await page
|
||||||
.getByRole( 'link', {
|
.getByRole( 'link', {
|
||||||
|
@ -224,7 +232,9 @@ test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => {
|
||||||
.getByPlaceholder( 'No brands' )
|
.getByPlaceholder( 'No brands' )
|
||||||
.pressSequentially( 'WooCommerce Apparels' );
|
.pressSequentially( 'WooCommerce Apparels' );
|
||||||
await page
|
await page
|
||||||
.getByRole( 'option', { name: 'WooCommerce Apparels' } )
|
.getByRole( 'option', {
|
||||||
|
name: 'WooCommerce Apparels',
|
||||||
|
} )
|
||||||
.click();
|
.click();
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
@ -272,7 +282,9 @@ test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => {
|
||||||
.click();
|
.click();
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder( 'No restrictions' )
|
.getByPlaceholder( 'No restrictions' )
|
||||||
.fill( couponData[ couponType ].allowedEmails[ 0 ] );
|
.fill(
|
||||||
|
couponData[ couponType ].allowedEmails[ 0 ]
|
||||||
|
);
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
// set usage limit
|
// set usage limit
|
||||||
|
@ -312,7 +324,9 @@ test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => {
|
||||||
|
|
||||||
// verify the creation of the coupon and basic details
|
// verify the creation of the coupon and basic details
|
||||||
await test.step( 'verify coupon creation', async () => {
|
await test.step( 'verify coupon creation', async () => {
|
||||||
await page.goto( 'wp-admin/edit.php?post_type=shop_coupon' );
|
await page.goto(
|
||||||
|
'wp-admin/edit.php?post_type=shop_coupon'
|
||||||
|
);
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole( 'cell', {
|
page.getByRole( 'cell', {
|
||||||
name: couponData[ couponType ].code,
|
name: couponData[ couponType ].code,
|
||||||
|
@ -491,9 +505,12 @@ test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => {
|
||||||
.click();
|
.click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByLabel( 'Usage limit per user' )
|
page.getByLabel( 'Usage limit per user' )
|
||||||
).toHaveValue( couponData[ couponType ].usageLimitPerUser );
|
).toHaveValue(
|
||||||
|
couponData[ couponType ].usageLimitPerUser
|
||||||
|
);
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
} );
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -60,8 +60,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
// this shipping zone already exists, don't create it
|
// this shipping zone already exists, don't create it
|
||||||
} else {
|
} else {
|
||||||
await page.goto(
|
await page.goto(
|
||||||
'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new',
|
'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new'
|
||||||
{ waitUntil: 'networkidle' }
|
|
||||||
);
|
);
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder( 'Zone name' )
|
.getByPlaceholder( 'Zone name' )
|
||||||
|
@ -92,10 +91,8 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
.getByRole( 'button', { name: 'Continue' } )
|
.getByRole( 'button', { name: 'Continue' } )
|
||||||
.last()
|
.last()
|
||||||
.click();
|
.click();
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await page.locator( '#btn-ok' ).click();
|
await page.locator( '#btn-ok' ).click();
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page
|
page
|
||||||
|
@ -132,8 +129,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
// this shipping zone already exists, don't create it
|
// this shipping zone already exists, don't create it
|
||||||
} else {
|
} else {
|
||||||
await page.goto(
|
await page.goto(
|
||||||
'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new',
|
'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new'
|
||||||
{ waitUntil: 'networkidle' }
|
|
||||||
);
|
);
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder( 'Zone name' )
|
.getByPlaceholder( 'Zone name' )
|
||||||
|
@ -159,10 +155,8 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
.getByRole( 'button', { name: 'Continue' } )
|
.getByRole( 'button', { name: 'Continue' } )
|
||||||
.last()
|
.last()
|
||||||
.click();
|
.click();
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await page.locator( '#btn-ok' ).click();
|
await page.locator( '#btn-ok' ).click();
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page
|
page
|
||||||
|
@ -196,8 +190,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
// this shipping zone already exists, don't create it
|
// this shipping zone already exists, don't create it
|
||||||
} else {
|
} else {
|
||||||
await page.goto(
|
await page.goto(
|
||||||
'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new',
|
'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new'
|
||||||
{ waitUntil: 'networkidle' }
|
|
||||||
);
|
);
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder( 'Zone name' )
|
.getByPlaceholder( 'Zone name' )
|
||||||
|
@ -209,7 +202,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
input.click();
|
input.click();
|
||||||
input.fill( 'Canada' );
|
input.fill( 'Canada' );
|
||||||
|
|
||||||
await page.getByText( 'Canada' ).last().click();
|
await page.getByLabel( 'Canada', { exact: true } ).click();
|
||||||
|
|
||||||
// Close dropdown
|
// Close dropdown
|
||||||
await page.getByPlaceholder( 'Zone name' ).click();
|
await page.getByPlaceholder( 'Zone name' ).click();
|
||||||
|
@ -222,10 +215,8 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
.getByRole( 'button', { name: 'Continue' } )
|
.getByRole( 'button', { name: 'Continue' } )
|
||||||
.last()
|
.last()
|
||||||
.click();
|
.click();
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await page.locator( '#btn-ok' ).click();
|
await page.locator( '#btn-ok' ).click();
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page
|
page
|
||||||
|
@ -240,7 +231,6 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
.click();
|
.click();
|
||||||
await page.getByLabel( 'Cost', { exact: true } ).fill( '10' );
|
await page.getByLabel( 'Cost', { exact: true } ).fill( '10' );
|
||||||
await page.getByRole( 'button', { name: 'Save' } ).last().click();
|
await page.getByRole( 'button', { name: 'Save' } ).last().click();
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await page.goto(
|
await page.goto(
|
||||||
'wp-admin/admin.php?page=wc-settings&tab=shipping'
|
'wp-admin/admin.php?page=wc-settings&tab=shipping'
|
||||||
|
@ -342,8 +332,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
// this shipping zone already exists, don't create it
|
// this shipping zone already exists, don't create it
|
||||||
} else {
|
} else {
|
||||||
await page.goto(
|
await page.goto(
|
||||||
'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new',
|
'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new'
|
||||||
{ waitUntil: 'networkidle' }
|
|
||||||
);
|
);
|
||||||
await page.locator( '#zone_name' ).fill( shippingZoneNameFlatRate );
|
await page.locator( '#zone_name' ).fill( shippingZoneNameFlatRate );
|
||||||
|
|
||||||
|
@ -353,7 +342,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
input.click();
|
input.click();
|
||||||
input.type( 'Canada' );
|
input.type( 'Canada' );
|
||||||
|
|
||||||
await page.getByText( 'Canada' ).last().click();
|
await page.getByLabel( 'Canada', { exact: true } ).click();
|
||||||
|
|
||||||
// Close dropdown
|
// Close dropdown
|
||||||
await page.keyboard.press( 'Escape' );
|
await page.keyboard.press( 'Escape' );
|
||||||
|
@ -366,10 +355,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
.last()
|
.last()
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await page.locator( '#btn-ok' ).click();
|
await page.locator( '#btn-ok' ).click();
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page
|
page
|
||||||
|
@ -384,13 +370,17 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
.click();
|
.click();
|
||||||
await page.locator( '#woocommerce_flat_rate_cost' ).fill( '10' );
|
await page.locator( '#woocommerce_flat_rate_cost' ).fill( '10' );
|
||||||
await page.locator( '#btn-ok' ).click();
|
await page.locator( '#btn-ok' ).click();
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await page.locator( 'text=Delete' ).waitFor();
|
await expect(
|
||||||
|
page.getByRole( 'cell', { name: 'Edit | Delete', exact: true } )
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
page.on( 'dialog', ( dialog ) => dialog.accept() );
|
page.on( 'dialog', ( dialog ) => dialog.accept() );
|
||||||
|
|
||||||
await page.locator( 'text=Delete' ).click();
|
await page
|
||||||
|
.getByRole( 'cell', { name: 'Edit | Delete', exact: true } )
|
||||||
|
.locator( 'text=Delete' )
|
||||||
|
.click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator( '.wc-shipping-zone-method-blank-state' )
|
page.locator( '.wc-shipping-zone-method-blank-state' )
|
||||||
|
@ -482,7 +472,6 @@ test.describe( 'Verifies shipping options from customer perspective', () => {
|
||||||
await context.clearCookies();
|
await context.clearCookies();
|
||||||
|
|
||||||
await page.goto( `/shop/?add-to-cart=${ productId }` );
|
await page.goto( `/shop/?add-to-cart=${ productId }` );
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
} );
|
} );
|
||||||
|
|
||||||
test.afterAll( async ( { baseURL } ) => {
|
test.afterAll( async ( { baseURL } ) => {
|
||||||
|
|
|
@ -52,7 +52,14 @@ const test = baseTest.extend( {
|
||||||
|
|
||||||
test.describe(
|
test.describe(
|
||||||
'Add WooCommerce Blocks Into Page',
|
'Add WooCommerce Blocks Into Page',
|
||||||
{ tag: [ '@gutenberg', '@services', '@skip-on-default-pressable' ] },
|
{
|
||||||
|
tag: [
|
||||||
|
'@gutenberg',
|
||||||
|
'@services',
|
||||||
|
'@skip-on-default-pressable',
|
||||||
|
'@skip-on-default-wpcom',
|
||||||
|
],
|
||||||
|
},
|
||||||
() => {
|
() => {
|
||||||
test.beforeAll( async ( { api } ) => {
|
test.beforeAll( async ( { api } ) => {
|
||||||
// add product attribute
|
// add product attribute
|
||||||
|
|
|
@ -28,7 +28,14 @@ const test = baseTest.extend( {
|
||||||
|
|
||||||
test.describe(
|
test.describe(
|
||||||
'Add WooCommerce Patterns Into Page',
|
'Add WooCommerce Patterns Into Page',
|
||||||
{ tag: [ '@gutenberg', '@services', '@skip-on-default-pressable' ] },
|
{
|
||||||
|
tag: [
|
||||||
|
'@gutenberg',
|
||||||
|
'@services',
|
||||||
|
'@skip-on-default-pressable',
|
||||||
|
'@skip-on-default-wpcom',
|
||||||
|
],
|
||||||
|
},
|
||||||
() => {
|
() => {
|
||||||
test( 'can insert WooCommerce patterns into page', async ( {
|
test( 'can insert WooCommerce patterns into page', async ( {
|
||||||
page,
|
page,
|
||||||
|
@ -86,7 +93,9 @@ test.describe(
|
||||||
// check some elements from added patterns
|
// check some elements from added patterns
|
||||||
for ( let i = 1; i < wooPatterns.length; i++ ) {
|
for ( let i = 1; i < wooPatterns.length; i++ ) {
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText( `${ wooPatterns[ i ].button }` )
|
page.getByRole( 'link', {
|
||||||
|
name: `${ wooPatterns[ i ].button }`,
|
||||||
|
} )
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -85,7 +85,7 @@ test.describe( 'Merchant > Customer List', { tag: '@services' }, () => {
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'Merchant can view a list of all customers, filter and download',
|
'Merchant can view a list of all customers, filter and download',
|
||||||
{ tag: '@skip-on-default-pressable' },
|
{ tag: [ '@skip-on-default-pressable', '@skip-on-default-wpcom' ] },
|
||||||
async ( { page, customers } ) => {
|
async ( { page, customers } ) => {
|
||||||
await test.step( 'Go to the customers reports page', async () => {
|
await test.step( 'Go to the customers reports page', async () => {
|
||||||
const responsePromise = page.waitForResponse(
|
const responsePromise = page.waitForResponse(
|
||||||
|
|
|
@ -110,6 +110,17 @@ test.describe(
|
||||||
await test.step( 'Select payment method and pay for the order', async () => {
|
await test.step( 'Select payment method and pay for the order', async () => {
|
||||||
// explicitly select the payment method
|
// explicitly select the payment method
|
||||||
await page.getByText( 'Direct bank transfer' ).click();
|
await page.getByText( 'Direct bank transfer' ).click();
|
||||||
|
|
||||||
|
// Handle notice if present
|
||||||
|
await page.addLocatorHandler(
|
||||||
|
page.getByRole( 'link', { name: 'Dismiss' } ),
|
||||||
|
async () => {
|
||||||
|
await page
|
||||||
|
.getByRole( 'link', { name: 'Dismiss' } )
|
||||||
|
.click();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// pay for the order
|
// pay for the order
|
||||||
await page
|
await page
|
||||||
.getByRole( 'button', { name: 'Pay for order' } )
|
.getByRole( 'button', { name: 'Pay for order' } )
|
||||||
|
|
|
@ -4,7 +4,7 @@ const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default;
|
||||||
|
|
||||||
test.describe(
|
test.describe(
|
||||||
'Launch Your Store - logged in',
|
'Launch Your Store - logged in',
|
||||||
{ tag: [ '@gutenberg', '@services' ] },
|
{ tag: [ '@gutenberg', '@services', '@skip-on-default-wpcom' ] },
|
||||||
() => {
|
() => {
|
||||||
test.use( { storageState: process.env.ADMINSTATE } );
|
test.use( { storageState: process.env.ADMINSTATE } );
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for the product reviews REST API.
|
* Tests for the product reviews REST API.
|
||||||
*
|
*
|
||||||
|
@ -50,7 +53,8 @@ class WC_Tests_API_Product_Reviews_V2 extends WC_REST_Unit_Test_Case {
|
||||||
|
|
||||||
$this->assertEquals( 200, $response->get_status() );
|
$this->assertEquals( 200, $response->get_status() );
|
||||||
$this->assertEquals( 10, count( $product_reviews ) );
|
$this->assertEquals( 10, count( $product_reviews ) );
|
||||||
$this->assertContains(
|
$this->assertEmpty(
|
||||||
|
ArrayUtil::deep_assoc_array_diff(
|
||||||
array(
|
array(
|
||||||
'id' => $review_id,
|
'id' => $review_id,
|
||||||
'date_created' => $product_reviews[0]['date_created'],
|
'date_created' => $product_reviews[0]['date_created'],
|
||||||
|
@ -83,7 +87,8 @@ class WC_Tests_API_Product_Reviews_V2 extends WC_REST_Unit_Test_Case {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
$product_reviews
|
$product_reviews[0]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||||
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
|
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -482,7 +483,16 @@ class Settings_V2 extends WC_REST_Unit_Test_Case {
|
||||||
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/products' ) );
|
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/products' ) );
|
||||||
$data = $response->get_data();
|
$data = $response->get_data();
|
||||||
$this->assertTrue( is_array( $data ) );
|
$this->assertTrue( is_array( $data ) );
|
||||||
$this->assertContains(
|
$data_download_required_login = null;
|
||||||
|
foreach ( $data as $setting ) {
|
||||||
|
if ( 'woocommerce_downloads_require_login' === $setting['id'] ) {
|
||||||
|
$data_download_required_login = $setting;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->assertNotEmpty( $data_download_required_login );
|
||||||
|
$this->assertEmpty(
|
||||||
|
ArrayUtil::deep_assoc_array_diff(
|
||||||
array(
|
array(
|
||||||
'id' => 'woocommerce_downloads_require_login',
|
'id' => 'woocommerce_downloads_require_login',
|
||||||
'label' => 'Access restriction',
|
'label' => 'Access restriction',
|
||||||
|
@ -504,7 +514,8 @@ class Settings_V2 extends WC_REST_Unit_Test_Case {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
$data
|
$data_download_required_login
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// test get single.
|
// test get single.
|
||||||
|
@ -540,7 +551,18 @@ class Settings_V2 extends WC_REST_Unit_Test_Case {
|
||||||
|
|
||||||
$this->assertEquals( 200, $response->get_status() );
|
$this->assertEquals( 200, $response->get_status() );
|
||||||
|
|
||||||
$this->assertContains(
|
$recipient_setting = null;
|
||||||
|
foreach ( $settings as $setting ) {
|
||||||
|
if ( 'recipient' === $setting['id'] ) {
|
||||||
|
$recipient_setting = $setting;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertNotEmpty( $recipient_setting );
|
||||||
|
|
||||||
|
$this->assertEmpty(
|
||||||
|
ArrayUtil::deep_assoc_array_diff(
|
||||||
array(
|
array(
|
||||||
'id' => 'recipient',
|
'id' => 'recipient',
|
||||||
'label' => 'Recipient(s)',
|
'label' => 'Recipient(s)',
|
||||||
|
@ -562,7 +584,8 @@ class Settings_V2 extends WC_REST_Unit_Test_Case {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
$settings
|
$recipient_setting
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// test get single.
|
// test get single.
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for the Shipping Methods REST API.
|
* Tests for the Shipping Methods REST API.
|
||||||
*
|
*
|
||||||
|
@ -44,7 +47,18 @@ class Shipping_Methods_V2 extends WC_REST_Unit_Test_Case {
|
||||||
$methods = $response->get_data();
|
$methods = $response->get_data();
|
||||||
|
|
||||||
$this->assertEquals( 200, $response->get_status() );
|
$this->assertEquals( 200, $response->get_status() );
|
||||||
$this->assertContains(
|
|
||||||
|
$free_shipping_method = null;
|
||||||
|
foreach ( $methods as $method ) {
|
||||||
|
if ( 'free_shipping' === $method['id'] ) {
|
||||||
|
$free_shipping_method = $method;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->assertNotEmpty( $free_shipping_method );
|
||||||
|
|
||||||
|
$this->assertEmpty(
|
||||||
|
ArrayUtil::deep_assoc_array_diff(
|
||||||
array(
|
array(
|
||||||
'id' => 'free_shipping',
|
'id' => 'free_shipping',
|
||||||
'title' => 'Free shipping',
|
'title' => 'Free shipping',
|
||||||
|
@ -62,7 +76,8 @@ class Shipping_Methods_V2 extends WC_REST_Unit_Test_Case {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
$methods
|
$free_shipping_method
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shipping Zones API Tests
|
* Shipping Zones API Tests
|
||||||
* @package WooCommerce\Tests\API
|
* @package WooCommerce\Tests\API
|
||||||
|
@ -74,7 +76,8 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case {
|
||||||
|
|
||||||
$this->assertEquals( 200, $response->get_status() );
|
$this->assertEquals( 200, $response->get_status() );
|
||||||
$this->assertEquals( count( $data ), 1 );
|
$this->assertEquals( count( $data ), 1 );
|
||||||
$this->assertContains(
|
$this->assertEmpty(
|
||||||
|
ArrayUtil::deep_assoc_array_diff(
|
||||||
array(
|
array(
|
||||||
'id' => $data[0]['id'],
|
'id' => $data[0]['id'],
|
||||||
'name' => 'Locations not covered by your other zones',
|
'name' => 'Locations not covered by your other zones',
|
||||||
|
@ -97,7 +100,8 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
$data
|
$data[0]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create a zone and make sure it's in the response
|
// Create a zone and make sure it's in the response
|
||||||
|
@ -108,7 +112,8 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case {
|
||||||
|
|
||||||
$this->assertEquals( 200, $response->get_status() );
|
$this->assertEquals( 200, $response->get_status() );
|
||||||
$this->assertEquals( count( $data ), 2 );
|
$this->assertEquals( count( $data ), 2 );
|
||||||
$this->assertContains(
|
$this->assertEmpty(
|
||||||
|
ArrayUtil::deep_assoc_array_diff(
|
||||||
array(
|
array(
|
||||||
'id' => $data[1]['id'],
|
'id' => $data[1]['id'],
|
||||||
'name' => 'Zone 1',
|
'name' => 'Zone 1',
|
||||||
|
@ -131,7 +136,8 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
$data
|
$data[1]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,7 +201,8 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case {
|
||||||
$data = $response->get_data();
|
$data = $response->get_data();
|
||||||
|
|
||||||
$this->assertEquals( 201, $response->get_status() );
|
$this->assertEquals( 201, $response->get_status() );
|
||||||
$this->assertEquals(
|
$this->assertEmpty(
|
||||||
|
ArrayUtil::deep_assoc_array_diff(
|
||||||
array(
|
array(
|
||||||
'id' => $data['id'],
|
'id' => $data['id'],
|
||||||
'name' => 'Test Zone',
|
'name' => 'Test Zone',
|
||||||
|
@ -219,6 +226,7 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
$data
|
$data
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,7 +268,8 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case {
|
||||||
$data = $response->get_data();
|
$data = $response->get_data();
|
||||||
|
|
||||||
$this->assertEquals( 200, $response->get_status() );
|
$this->assertEquals( 200, $response->get_status() );
|
||||||
$this->assertEquals(
|
$this->assertEmpty(
|
||||||
|
ArrayUtil::deep_assoc_array_diff(
|
||||||
array(
|
array(
|
||||||
'id' => $zone->get_id(),
|
'id' => $zone->get_id(),
|
||||||
'name' => 'Zone Test',
|
'name' => 'Zone Test',
|
||||||
|
@ -284,6 +293,7 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
$data
|
$data
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -359,7 +369,8 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case {
|
||||||
$data = $response->get_data();
|
$data = $response->get_data();
|
||||||
|
|
||||||
$this->assertEquals( 200, $response->get_status() );
|
$this->assertEquals( 200, $response->get_status() );
|
||||||
$this->assertEquals(
|
$this->assertEmpty(
|
||||||
|
ArrayUtil::deep_assoc_array_diff(
|
||||||
array(
|
array(
|
||||||
'id' => $zone->get_id(),
|
'id' => $zone->get_id(),
|
||||||
'name' => 'Test Zone',
|
'name' => 'Test Zone',
|
||||||
|
@ -383,6 +394,7 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
$data
|
$data
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -624,13 +636,13 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case {
|
||||||
|
|
||||||
$this->assertEquals( 200, $response->get_status() );
|
$this->assertEquals( 200, $response->get_status() );
|
||||||
$this->assertEquals( count( $data ), 1 );
|
$this->assertEquals( count( $data ), 1 );
|
||||||
$this->assertContains( $expected, $data );
|
$this->assertEmpty( ArrayUtil::deep_assoc_array_diff( $expected, $data[0] ) );
|
||||||
|
|
||||||
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ) );
|
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ) );
|
||||||
$data = $response->get_data();
|
$data = $response->get_data();
|
||||||
|
|
||||||
$this->assertEquals( 200, $response->get_status() );
|
$this->assertEquals( 200, $response->get_status() );
|
||||||
$this->assertEquals( $expected, $data );
|
$this->assertEmpty( ArrayUtil::deep_assoc_array_diff( $expected, $data ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for the product reviews REST API.
|
* Tests for the product reviews REST API.
|
||||||
*
|
*
|
||||||
|
@ -50,7 +53,8 @@ class WC_Tests_API_Product_Reviews extends WC_REST_Unit_Test_Case {
|
||||||
|
|
||||||
$this->assertEquals( 200, $response->get_status() );
|
$this->assertEquals( 200, $response->get_status() );
|
||||||
$this->assertEquals( 10, count( $product_reviews ) );
|
$this->assertEquals( 10, count( $product_reviews ) );
|
||||||
$this->assertContains(
|
$this->assertEmpty(
|
||||||
|
ArrayUtil::deep_assoc_array_diff(
|
||||||
array(
|
array(
|
||||||
'id' => $review_id,
|
'id' => $review_id,
|
||||||
'date_created' => $product_reviews[0]['date_created'],
|
'date_created' => $product_reviews[0]['date_created'],
|
||||||
|
@ -83,7 +87,8 @@ class WC_Tests_API_Product_Reviews extends WC_REST_Unit_Test_Case {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
$product_reviews
|
$product_reviews[0]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
* @since 3.5.0
|
* @since 3.5.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||||
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
|
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -481,7 +482,19 @@ class Settings extends WC_REST_Unit_Test_Case {
|
||||||
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/products' ) );
|
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/products' ) );
|
||||||
$data = $response->get_data();
|
$data = $response->get_data();
|
||||||
$this->assertTrue( is_array( $data ) );
|
$this->assertTrue( is_array( $data ) );
|
||||||
$this->assertContains(
|
|
||||||
|
$setting_downloads_required = null;
|
||||||
|
foreach ( $data as $setting ) {
|
||||||
|
if ( 'woocommerce_downloads_require_login' === $setting['id'] ) {
|
||||||
|
$setting_downloads_required = $setting;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertNotEmpty( $setting_downloads_required );
|
||||||
|
|
||||||
|
$this->assertEmpty(
|
||||||
|
ArrayUtil::deep_assoc_array_diff(
|
||||||
array(
|
array(
|
||||||
'id' => 'woocommerce_downloads_require_login',
|
'id' => 'woocommerce_downloads_require_login',
|
||||||
'label' => 'Access restriction',
|
'label' => 'Access restriction',
|
||||||
|
@ -503,7 +516,8 @@ class Settings extends WC_REST_Unit_Test_Case {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
$data
|
$setting_downloads_required
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// test get single.
|
// test get single.
|
||||||
|
@ -539,7 +553,18 @@ class Settings extends WC_REST_Unit_Test_Case {
|
||||||
|
|
||||||
$this->assertEquals( 200, $response->get_status() );
|
$this->assertEquals( 200, $response->get_status() );
|
||||||
|
|
||||||
$this->assertContains(
|
$recipient_setting = null;
|
||||||
|
foreach ( $settings as $setting ) {
|
||||||
|
if ( 'recipient' === $setting['id'] ) {
|
||||||
|
$recipient_setting = $setting;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertNotEmpty( $recipient_setting );
|
||||||
|
|
||||||
|
$this->assertEmpty(
|
||||||
|
ArrayUtil::deep_assoc_array_diff(
|
||||||
array(
|
array(
|
||||||
'id' => 'recipient',
|
'id' => 'recipient',
|
||||||
'label' => 'Recipient(s)',
|
'label' => 'Recipient(s)',
|
||||||
|
@ -561,7 +586,8 @@ class Settings extends WC_REST_Unit_Test_Case {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
$settings
|
$recipient_setting
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// test get single.
|
// test get single.
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for the Shipping Methods REST API.
|
* Tests for the Shipping Methods REST API.
|
||||||
*
|
*
|
||||||
|
@ -44,7 +47,18 @@ class Shipping_Methods extends WC_REST_Unit_Test_Case {
|
||||||
$methods = $response->get_data();
|
$methods = $response->get_data();
|
||||||
|
|
||||||
$this->assertEquals( 200, $response->get_status() );
|
$this->assertEquals( 200, $response->get_status() );
|
||||||
$this->assertContains(
|
|
||||||
|
$free_shipping = null;
|
||||||
|
foreach ( $methods as $method ) {
|
||||||
|
if ( 'free_shipping' === $method['id'] ) {
|
||||||
|
$free_shipping = $method;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->assertNotEmpty( $free_shipping );
|
||||||
|
|
||||||
|
$this->assertEmpty(
|
||||||
|
ArrayUtil::deep_assoc_array_diff(
|
||||||
array(
|
array(
|
||||||
'id' => 'free_shipping',
|
'id' => 'free_shipping',
|
||||||
'title' => 'Free shipping',
|
'title' => 'Free shipping',
|
||||||
|
@ -62,7 +76,8 @@ class Shipping_Methods extends WC_REST_Unit_Test_Case {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
$methods
|
$free_shipping
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shipping Zones API Tests
|
* Shipping Zones API Tests
|
||||||
*
|
*
|
||||||
|
@ -77,7 +79,8 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case {
|
||||||
|
|
||||||
$this->assertEquals( 200, $response->get_status() );
|
$this->assertEquals( 200, $response->get_status() );
|
||||||
$this->assertEquals( count( $data ), 1 );
|
$this->assertEquals( count( $data ), 1 );
|
||||||
$this->assertContains(
|
$this->assertEmpty(
|
||||||
|
ArrayUtil::deep_assoc_array_diff(
|
||||||
array(
|
array(
|
||||||
'id' => $data[0]['id'],
|
'id' => $data[0]['id'],
|
||||||
'name' => 'Locations not covered by your other zones',
|
'name' => 'Locations not covered by your other zones',
|
||||||
|
@ -100,7 +103,8 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
$data
|
$data[0]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create a zone and make sure it's in the response
|
// Create a zone and make sure it's in the response
|
||||||
|
@ -111,7 +115,8 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case {
|
||||||
|
|
||||||
$this->assertEquals( 200, $response->get_status() );
|
$this->assertEquals( 200, $response->get_status() );
|
||||||
$this->assertEquals( count( $data ), 2 );
|
$this->assertEquals( count( $data ), 2 );
|
||||||
$this->assertContains(
|
$this->assertEmpty(
|
||||||
|
ArrayUtil::deep_assoc_array_diff(
|
||||||
array(
|
array(
|
||||||
'id' => $data[1]['id'],
|
'id' => $data[1]['id'],
|
||||||
'name' => 'Zone 1',
|
'name' => 'Zone 1',
|
||||||
|
@ -134,7 +139,8 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
$data
|
$data[1]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,7 +208,8 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case {
|
||||||
$data = $response->get_data();
|
$data = $response->get_data();
|
||||||
|
|
||||||
$this->assertEquals( 201, $response->get_status() );
|
$this->assertEquals( 201, $response->get_status() );
|
||||||
$this->assertEquals(
|
$this->assertEmpty(
|
||||||
|
ArrayUtil::deep_assoc_array_diff(
|
||||||
array(
|
array(
|
||||||
'id' => $data['id'],
|
'id' => $data['id'],
|
||||||
'name' => 'Test Zone',
|
'name' => 'Test Zone',
|
||||||
|
@ -226,6 +233,7 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
$data
|
$data
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,7 +277,8 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case {
|
||||||
$data = $response->get_data();
|
$data = $response->get_data();
|
||||||
|
|
||||||
$this->assertEquals( 200, $response->get_status() );
|
$this->assertEquals( 200, $response->get_status() );
|
||||||
$this->assertEquals(
|
$this->assertEmpty(
|
||||||
|
ArrayUtil::deep_assoc_array_diff(
|
||||||
array(
|
array(
|
||||||
'id' => $zone->get_id(),
|
'id' => $zone->get_id(),
|
||||||
'name' => 'Zone Test',
|
'name' => 'Zone Test',
|
||||||
|
@ -293,6 +302,7 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
$data
|
$data
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -373,7 +383,8 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case {
|
||||||
$data = $response->get_data();
|
$data = $response->get_data();
|
||||||
|
|
||||||
$this->assertEquals( 200, $response->get_status() );
|
$this->assertEquals( 200, $response->get_status() );
|
||||||
$this->assertEquals(
|
$this->assertEmpty(
|
||||||
|
ArrayUtil::deep_assoc_array_diff(
|
||||||
array(
|
array(
|
||||||
'id' => $zone->get_id(),
|
'id' => $zone->get_id(),
|
||||||
'name' => 'Test Zone',
|
'name' => 'Test Zone',
|
||||||
|
@ -397,6 +408,7 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
$data
|
$data
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -644,13 +656,12 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case {
|
||||||
|
|
||||||
$this->assertEquals( 200, $response->get_status() );
|
$this->assertEquals( 200, $response->get_status() );
|
||||||
$this->assertEquals( count( $data ), 1 );
|
$this->assertEquals( count( $data ), 1 );
|
||||||
$this->assertContains( $expected, $data );
|
$this->assertEmpty( ArrayUtil::deep_assoc_array_diff( $expected, $data[0] ) );
|
||||||
|
|
||||||
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ) );
|
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ) );
|
||||||
$data = $response->get_data();
|
$data = $response->get_data();
|
||||||
|
|
||||||
$this->assertEquals( 200, $response->get_status() );
|
$this->assertEquals( 200, $response->get_status() );
|
||||||
$this->assertEquals( $expected, $data );
|
$this->assertEmpty( ArrayUtil::deep_assoc_array_diff( $expected, $data ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -356,104 +356,4 @@ class WC_Stock_Functions_Tests extends \WC_Unit_Test_Case {
|
||||||
|
|
||||||
$this->assertIsIntAndEquals( $site_wide_low_stock_amount, wc_get_low_stock_amount( $var1 ) );
|
$this->assertIsIntAndEquals( $site_wide_low_stock_amount, wc_get_low_stock_amount( $var1 ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @testdox Test that the `woocommerce_low_stock` action fires when a product stock hits the low stock threshold.
|
|
||||||
*/
|
|
||||||
public function test_wc_update_product_stock_low_stock_action() {
|
|
||||||
$product = WC_Helper_Product::create_simple_product();
|
|
||||||
$product->set_manage_stock( true );
|
|
||||||
$product->save();
|
|
||||||
|
|
||||||
$low_stock_amount = wc_get_low_stock_amount( $product );
|
|
||||||
$initial_stock = $low_stock_amount + 2;
|
|
||||||
|
|
||||||
wc_update_product_stock( $product->get_id(), $initial_stock );
|
|
||||||
|
|
||||||
$action_fired = false;
|
|
||||||
$callback = function () use ( &$action_fired ) {
|
|
||||||
$action_fired = true;
|
|
||||||
};
|
|
||||||
add_action( 'woocommerce_low_stock', $callback );
|
|
||||||
|
|
||||||
// Test with `wc_update_product_stock`.
|
|
||||||
wc_update_product_stock( $product->get_id(), 1, 'decrease' );
|
|
||||||
$this->assertFalse( $action_fired );
|
|
||||||
wc_update_product_stock( $product->get_id(), 1, 'decrease' );
|
|
||||||
$this->assertTrue( $action_fired );
|
|
||||||
|
|
||||||
$action_fired = false;
|
|
||||||
|
|
||||||
// Test with the data store.
|
|
||||||
$product->set_stock_quantity( $initial_stock );
|
|
||||||
$product->save();
|
|
||||||
$this->assertFalse( $action_fired );
|
|
||||||
$product->set_stock_quantity( $low_stock_amount );
|
|
||||||
$product->save();
|
|
||||||
$this->assertTrue( $action_fired );
|
|
||||||
|
|
||||||
remove_action( 'woocommerce_low_stock', $callback );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @testdox Test that the `woocommerce_no_stock` action fires when a product stock hits the no stock threshold.
|
|
||||||
*/
|
|
||||||
public function test_wc_update_product_stock_no_stock_action() {
|
|
||||||
$product = WC_Helper_Product::create_simple_product();
|
|
||||||
$product->set_manage_stock( true );
|
|
||||||
$product->save();
|
|
||||||
|
|
||||||
$no_stock_amount = get_option( 'woocommerce_notify_no_stock_amount', 0 );
|
|
||||||
$initial_stock = $no_stock_amount + 2;
|
|
||||||
|
|
||||||
wc_update_product_stock( $product->get_id(), $initial_stock );
|
|
||||||
|
|
||||||
$action_fired = false;
|
|
||||||
$callback = function () use ( &$action_fired ) {
|
|
||||||
$action_fired = true;
|
|
||||||
};
|
|
||||||
add_action( 'woocommerce_no_stock', $callback );
|
|
||||||
|
|
||||||
// Test with `wc_update_product_stock`.
|
|
||||||
wc_update_product_stock( $product->get_id(), 1, 'decrease' );
|
|
||||||
$this->assertFalse( $action_fired );
|
|
||||||
wc_update_product_stock( $product->get_id(), 1, 'decrease' );
|
|
||||||
$this->assertTrue( $action_fired );
|
|
||||||
|
|
||||||
$action_fired = false;
|
|
||||||
|
|
||||||
// Test with the data store.
|
|
||||||
$product->set_stock_quantity( $initial_stock );
|
|
||||||
$product->save();
|
|
||||||
$this->assertFalse( $action_fired );
|
|
||||||
$product->set_stock_quantity( $no_stock_amount );
|
|
||||||
$product->save();
|
|
||||||
$this->assertTrue( $action_fired );
|
|
||||||
|
|
||||||
remove_action( 'woocommerce_no_stock', $callback );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @testdox The wc_trigger_stock_change_actions function should only trigger actions if the product is set
|
|
||||||
* to manage stock.
|
|
||||||
*/
|
|
||||||
public function test_wc_trigger_stock_change_actions_bails_early_for_unmanaged_stock() {
|
|
||||||
$action_fired = false;
|
|
||||||
$callback = function () use ( &$action_fired ) {
|
|
||||||
$action_fired = true;
|
|
||||||
};
|
|
||||||
add_action( 'woocommerce_no_stock', $callback );
|
|
||||||
|
|
||||||
$product = WC_Helper_Product::create_simple_product();
|
|
||||||
|
|
||||||
$this->assertFalse( $action_fired );
|
|
||||||
|
|
||||||
$product->set_manage_stock( true );
|
|
||||||
$product->set_stock_quantity( 0 );
|
|
||||||
$product->save();
|
|
||||||
|
|
||||||
$this->assertTrue( $action_fired );
|
|
||||||
|
|
||||||
remove_action( 'woocommerce_no_stock', $callback );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue