Product Collection - Show product picker in Editor when collection requires a product but it doesn't exist (#50164)
* Show product picker control in the editor when a product context is required but not provided Enhanced the Product Collection block by introducing the `selectedReference` attribute and implementing a product picker control. This control appears in the editor when a product context is required but not provided in the current template/page/post. 1. **block.json**: Added `selectedReference` attribute of type `object`. 2. **constants.ts**: Included `selectedReference` in the `queryContextIncludes` array. 3. **EditorProductPicker.tsx**: Created a new component for selecting products within the editor. 4. **editor.scss**: Added styles for the new Editor Product Picker component. 5. **index.tsx**: Updated logic to determine the component to render, incorporating the new Editor Product Picker. 6. **types.ts**: Defined types for `selectedReference` and updated `ProductCollectionAttributes` interface. This enhancement allows merchants to manually select a product for collections that require a product context, ensuring the block displays correctly even when the product context is not available in the current template/page/post. - **Product Picker Control**: Utilizes the existing `ProductControl` component for selecting products. This component is displayed in the editor when a collection requires a product context but it doesn't exist in the current template/page/post. * Update label on ProductControl component * Implement dynamic UI state management for product collection block - Introduced `ProductCollectionUIStatesInEditor` enum to define various UI states for the product collection block. - Added `getProductCollectionUIStateInEditor` utility function to determine the appropriate UI state based on context. - Updated `Edit` component to use `getProductCollectionUIStateInEditor` for dynamic state management. - Refactored `ProductCollectionContent` to utilize the new Editor UI state management. * Fix: Product picker isn't showing * Fix: Preview label state isn't showing * Add changefile(s) from automation for the following project(s): woocommerce-blocks * Refactor WooCommerceBlockLocation type - Introduced specific interfaces for WooCommerceBlockLocation, including ProductLocation, ArchiveLocation, CartLocation, OrderLocation, and SiteLocation, to improve type safety and code clarity. - Updated createLocationObject function to return a BaseLocation type. - Refactored useSetPreviewState hook in product-collection utils: - Extracted termId from location.sourceData for cleaner and more readable code. - Replaced direct access of location.sourceData?.termId with termId variable. * Remove fallback to 0 in case there may be a product with id 0 * Use optional chaining to avoid undefined errors * Rename to * Change order of arguments in function * Pass boolean prop instead of making further recognition of the UI state in ProductCollectionContent * Destructure props in component * Rename to * Update names in enum * Rename to and change the structure to single number. * Rename location to * Add a method to choose a product in the product picker in Editor * Add E2E tests * Fix failing e2e tests by changing location to productCollectionLocation * Add changefile(s) from automation for the following project(s): woocommerce-blocks * Don't allow selecting product variations * Minor code refactoring * Fix: Product control isn't showing products **Before** ```tsx const getRenderItemFunc = () => { if ( renderItem ) { return renderItem; } else if ( showVariations ) { return renderItemWithVariations; } return () => null; }; ``` As you can see above, `return () => null;` is returning a function which is causing the issue. This will render nothing in the list. I changed this to `return undefined;`. This way, we will use default render item function. * Translate text in ProductPicker component * Improve E2E test * Use createInterpolateElement to safely render strong HTML tag * Fix E2E tests * Fix E2E tests * Product Collection: Inspector control to change selected product (#50590) * Add Linked Product Control to Product Collection Block Inspector Controls - Introduced a new `LinkedProductControl` component in the Product Collection block's Inspector Controls. - This control allows users to link a specific product to the product collection via a dropdown with a search capability. - Added corresponding styles to `editor.scss`. - Integrated a `useGetProduct` hook in the `utils.tsx` to fetch and manage the state of the linked product data, including handling loading states and errors. - Updated the Inspector Controls to include the new Linked Product Control component, enhancing the block's customization options for users. * Add E2E tests * Hide product picker when product context is available * Improve logic to hide Linked Product Control * Add changefile(s) from automation for the following project(s): woocommerce-blocks * Remove hasError state from useGetProduct hook * Rename isShowLinkedProductControl to showLinkedProductControl * Convert jsxProductButton to ProductButton component * Refactor jsxPopoverContent to LinkedProductPopoverContent component * Improve UI of Linked Product Control * Address PR feedback * Fix E2E tests --------- Co-authored-by: github-actions <github-actions@github.com> * Rename isUsesReferencePreviewMode to isUsingReferencePreviewMode * Change order of conditions in getProductCollectionUIStateInEditor --------- Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
parent
36ede651db
commit
2433664aa8
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import { Icon, info } from '@wordpress/icons';
|
||||
import ProductControl from '@woocommerce/editor-components/product-control';
|
||||
import type { SelectedOption } from '@woocommerce/block-hocs';
|
||||
import { createInterpolateElement } from '@wordpress/element';
|
||||
import {
|
||||
Placeholder,
|
||||
// @ts-expect-error Using experimental features
|
||||
__experimentalHStack as HStack,
|
||||
// @ts-expect-error Using experimental features
|
||||
__experimentalText as Text,
|
||||
} from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { ProductCollectionEditComponentProps } from '../types';
|
||||
import { getCollectionByName } from '../collections';
|
||||
|
||||
const ProductPicker = ( props: ProductCollectionEditComponentProps ) => {
|
||||
const blockProps = useBlockProps();
|
||||
const attributes = props.attributes;
|
||||
|
||||
const collection = getCollectionByName( attributes.collection );
|
||||
if ( ! collection ) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<Placeholder className="wc-blocks-product-collection__editor-product-picker">
|
||||
<HStack alignment="center">
|
||||
<Icon
|
||||
icon={ info }
|
||||
className="wc-blocks-product-collection__info-icon"
|
||||
/>
|
||||
<Text>
|
||||
{ createInterpolateElement(
|
||||
sprintf(
|
||||
/* translators: %s: collection title */
|
||||
__(
|
||||
'<strong>%s</strong> requires a product to be selected in order to display associated items.',
|
||||
'woocommerce'
|
||||
),
|
||||
collection.title
|
||||
),
|
||||
{
|
||||
strong: <strong />,
|
||||
}
|
||||
) }
|
||||
</Text>
|
||||
</HStack>
|
||||
<ProductControl
|
||||
selected={
|
||||
attributes.query?.productReference as SelectedOption
|
||||
}
|
||||
onChange={ ( value = [] ) => {
|
||||
const isValidId = ( value[ 0 ]?.id ?? null ) !== null;
|
||||
if ( isValidId ) {
|
||||
props.setAttributes( {
|
||||
query: {
|
||||
...attributes.query,
|
||||
productReference: value[ 0 ].id,
|
||||
},
|
||||
} );
|
||||
}
|
||||
} }
|
||||
messages={ {
|
||||
search: __( 'Select a product', 'woocommerce' ),
|
||||
} }
|
||||
/>
|
||||
</Placeholder>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductPicker;
|
|
@ -168,3 +168,49 @@ $max-button-width: calc(100% / #{$max-button-columns});
|
|||
color: var(--wp-components-color-accent-inverted, #fff);
|
||||
}
|
||||
}
|
||||
|
||||
// Editor Product Picker
|
||||
.wc-blocks-product-collection__editor-product-picker {
|
||||
.wc-blocks-product-collection__info-icon {
|
||||
fill: var(--wp--preset--color--luminous-vivid-orange, #e26f56);
|
||||
}
|
||||
}
|
||||
|
||||
// Linked Product Control
|
||||
.wc-block-product-collection-linked-product-control {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
&__button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid $gray-300;
|
||||
}
|
||||
|
||||
&__image-container {
|
||||
flex-shrink: 0;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-product-collection-linked-product__popover-content .components-popover__content {
|
||||
width: 100%;
|
||||
|
||||
.woocommerce-search-list__search {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,18 +4,25 @@
|
|||
import { store as blockEditorStore } from '@wordpress/block-editor';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { useGetLocation } from '@woocommerce/blocks/product-template/utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { ProductCollectionEditComponentProps } from '../types';
|
||||
import {
|
||||
ProductCollectionEditComponentProps,
|
||||
ProductCollectionUIStatesInEditor,
|
||||
} from '../types';
|
||||
import ProductCollectionPlaceholder from './product-collection-placeholder';
|
||||
import ProductCollectionContent from './product-collection-content';
|
||||
import CollectionSelectionModal from './collection-selection-modal';
|
||||
import './editor.scss';
|
||||
import { getProductCollectionUIStateInEditor } from '../utils';
|
||||
import ProductPicker from './ProductPicker';
|
||||
|
||||
const Edit = ( props: ProductCollectionEditComponentProps ) => {
|
||||
const { clientId, attributes } = props;
|
||||
const location = useGetLocation( props.context, props.clientId );
|
||||
|
||||
const [ isSelectionModalOpen, setIsSelectionModalOpen ] = useState( false );
|
||||
const hasInnerBlocks = useSelect(
|
||||
|
@ -24,9 +31,37 @@ const Edit = ( props: ProductCollectionEditComponentProps ) => {
|
|||
[ clientId ]
|
||||
);
|
||||
|
||||
const Component = hasInnerBlocks
|
||||
? ProductCollectionContent
|
||||
: ProductCollectionPlaceholder;
|
||||
const productCollectionUIStateInEditor =
|
||||
getProductCollectionUIStateInEditor( {
|
||||
hasInnerBlocks,
|
||||
location,
|
||||
attributes: props.attributes,
|
||||
usesReference: props.usesReference,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Component to render based on the UI state.
|
||||
*/
|
||||
let Component,
|
||||
isUsingReferencePreviewMode = false;
|
||||
switch ( productCollectionUIStateInEditor ) {
|
||||
case ProductCollectionUIStatesInEditor.COLLECTION_PICKER:
|
||||
Component = ProductCollectionPlaceholder;
|
||||
break;
|
||||
case ProductCollectionUIStatesInEditor.PRODUCT_REFERENCE_PICKER:
|
||||
Component = ProductPicker;
|
||||
break;
|
||||
case ProductCollectionUIStatesInEditor.VALID:
|
||||
Component = ProductCollectionContent;
|
||||
break;
|
||||
case ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW:
|
||||
Component = ProductCollectionContent;
|
||||
isUsingReferencePreviewMode = true;
|
||||
break;
|
||||
default:
|
||||
// By default showing collection chooser.
|
||||
Component = ProductCollectionPlaceholder;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -35,6 +70,9 @@ const Edit = ( props: ProductCollectionEditComponentProps ) => {
|
|||
openCollectionSelectionModal={ () =>
|
||||
setIsSelectionModalOpen( true )
|
||||
}
|
||||
isUsingReferencePreviewMode={ isUsingReferencePreviewMode }
|
||||
location={ location }
|
||||
usesReference={ props.usesReference }
|
||||
/>
|
||||
{ isSelectionModalOpen && (
|
||||
<CollectionSelectionModal
|
||||
|
|
|
@ -50,6 +50,7 @@ import LayoutOptionsControl from './layout-options-control';
|
|||
import FeaturedProductsControl from './featured-products-control';
|
||||
import CreatedControl from './created-control';
|
||||
import PriceRangeControl from './price-range-control';
|
||||
import LinkedProductControl from './linked-product-control';
|
||||
|
||||
const prepareShouldShowFilter =
|
||||
( hideControls: FilterName[] ) => ( filter: FilterName ) => {
|
||||
|
@ -121,6 +122,13 @@ const ProductCollectionInspectorControls = (
|
|||
|
||||
return (
|
||||
<InspectorControls>
|
||||
<LinkedProductControl
|
||||
query={ props.attributes.query }
|
||||
setAttributes={ props.setAttributes }
|
||||
usesReference={ props.usesReference }
|
||||
location={ props.location }
|
||||
/>
|
||||
|
||||
<ToolsPanel
|
||||
label={ __( 'Settings', 'woocommerce' ) }
|
||||
resetAll={ () => {
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import ProductControl from '@woocommerce/editor-components/product-control';
|
||||
import { SelectedOption } from '@woocommerce/block-hocs';
|
||||
import { useState, useMemo } from '@wordpress/element';
|
||||
import type { WooCommerceBlockLocation } from '@woocommerce/blocks/product-template/utils';
|
||||
import type { ProductResponseItem } from '@woocommerce/types';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import {
|
||||
PanelBody,
|
||||
PanelRow,
|
||||
Button,
|
||||
Flex,
|
||||
FlexItem,
|
||||
Dropdown,
|
||||
// @ts-expect-error Using experimental features
|
||||
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||
__experimentalText as Text,
|
||||
Spinner,
|
||||
} from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useGetProduct } from '../../utils';
|
||||
import type {
|
||||
ProductCollectionQuery,
|
||||
ProductCollectionSetAttributes,
|
||||
} from '../../types';
|
||||
|
||||
const ProductButton: React.FC< {
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
product: ProductResponseItem | null;
|
||||
isLoading: boolean;
|
||||
} > = ( { isOpen, onToggle, product, isLoading } ) => {
|
||||
if ( isLoading && ! product ) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="wc-block-product-collection-linked-product-control__button"
|
||||
onClick={ onToggle }
|
||||
aria-expanded={ isOpen }
|
||||
disabled={ isLoading }
|
||||
>
|
||||
<Flex direction="row" expanded justify="flex-start">
|
||||
<FlexItem className="wc-block-product-collection-linked-product-control__image-container">
|
||||
<img
|
||||
src={ product?.images?.[ 0 ]?.src }
|
||||
alt={ product?.name }
|
||||
/>
|
||||
</FlexItem>
|
||||
|
||||
<Flex
|
||||
direction="column"
|
||||
align="flex-start"
|
||||
gap={ 1 }
|
||||
className="wc-block-product-collection-linked-product-control__content"
|
||||
>
|
||||
<FlexItem>
|
||||
<Text color="inherit" lineHeight={ 1 }>
|
||||
{ product?.name
|
||||
? decodeEntities( product.name )
|
||||
: '' }
|
||||
</Text>
|
||||
</FlexItem>
|
||||
<FlexItem>
|
||||
<Text color="inherit" lineHeight={ 1 }>
|
||||
{ product?.sku }
|
||||
</Text>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const LinkedProductPopoverContent: React.FC< {
|
||||
query: ProductCollectionQuery;
|
||||
setAttributes: ProductCollectionSetAttributes;
|
||||
setIsDropdownOpen: React.Dispatch< React.SetStateAction< boolean > >;
|
||||
} > = ( { query, setAttributes, setIsDropdownOpen } ) => (
|
||||
<ProductControl
|
||||
selected={ query?.productReference as SelectedOption }
|
||||
onChange={ ( value: SelectedOption[] = [] ) => {
|
||||
const productId = value[ 0 ]?.id ?? null;
|
||||
if ( productId !== null ) {
|
||||
setAttributes( {
|
||||
query: {
|
||||
...query,
|
||||
productReference: productId,
|
||||
},
|
||||
} );
|
||||
setIsDropdownOpen( false );
|
||||
}
|
||||
} }
|
||||
messages={ {
|
||||
search: __( 'Select a product', 'woocommerce' ),
|
||||
} }
|
||||
/>
|
||||
);
|
||||
|
||||
const LinkedProductControl = ( {
|
||||
query,
|
||||
setAttributes,
|
||||
location,
|
||||
usesReference,
|
||||
}: {
|
||||
query: ProductCollectionQuery;
|
||||
setAttributes: ProductCollectionSetAttributes;
|
||||
location: WooCommerceBlockLocation;
|
||||
usesReference: string[] | undefined;
|
||||
} ) => {
|
||||
const [ isDropdownOpen, setIsDropdownOpen ] = useState< boolean >( false );
|
||||
const { product, isLoading } = useGetProduct( query.productReference );
|
||||
|
||||
const showLinkedProductControl = useMemo( () => {
|
||||
const isInRequiredLocation = usesReference?.includes( location.type );
|
||||
const isProductContextRequired = usesReference?.includes( 'product' );
|
||||
const isProductContextSelected =
|
||||
( query?.productReference ?? null ) !== null;
|
||||
|
||||
return (
|
||||
isProductContextRequired &&
|
||||
! isInRequiredLocation &&
|
||||
isProductContextSelected
|
||||
);
|
||||
}, [ location.type, query?.productReference, usesReference ] );
|
||||
|
||||
if ( ! showLinkedProductControl ) return null;
|
||||
|
||||
return (
|
||||
<PanelBody title={ __( 'Linked Product', 'woocommerce' ) }>
|
||||
<PanelRow>
|
||||
<Dropdown
|
||||
className="wc-block-product-collection-linked-product-control"
|
||||
contentClassName="wc-block-product-collection-linked-product__popover-content"
|
||||
popoverProps={ { placement: 'left-start' } }
|
||||
renderToggle={ ( { isOpen, onToggle } ) => (
|
||||
<ProductButton
|
||||
isOpen={ isOpen }
|
||||
onToggle={ onToggle }
|
||||
product={ product }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
) }
|
||||
renderContent={ () => (
|
||||
<LinkedProductPopoverContent
|
||||
query={ query }
|
||||
setAttributes={ setAttributes }
|
||||
setIsDropdownOpen={ setIsDropdownOpen }
|
||||
/>
|
||||
) }
|
||||
open={ isDropdownOpen }
|
||||
onToggle={ () => setIsDropdownOpen( ! isDropdownOpen ) }
|
||||
/>
|
||||
</PanelRow>
|
||||
</PanelBody>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinkedProductControl;
|
|
@ -10,7 +10,6 @@ import { useInstanceId } from '@wordpress/compose';
|
|||
import { useEffect, useRef, useMemo } from '@wordpress/element';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { useGetLocation } from '@woocommerce/blocks/product-template/utils';
|
||||
import fastDeepEqual from 'fast-deep-equal/es6';
|
||||
|
||||
/**
|
||||
|
@ -68,19 +67,23 @@ const useQueryId = (
|
|||
|
||||
const ProductCollectionContent = ( {
|
||||
preview: { setPreviewState, initialPreviewState } = {},
|
||||
usesReference,
|
||||
...props
|
||||
}: ProductCollectionEditComponentProps ) => {
|
||||
const isInitialAttributesSet = useRef( false );
|
||||
const { clientId, attributes, setAttributes } = props;
|
||||
const location = useGetLocation( props.context, props.clientId );
|
||||
const {
|
||||
clientId,
|
||||
attributes,
|
||||
setAttributes,
|
||||
location,
|
||||
isUsingReferencePreviewMode,
|
||||
} = props;
|
||||
|
||||
useSetPreviewState( {
|
||||
setPreviewState,
|
||||
setAttributes,
|
||||
location,
|
||||
attributes,
|
||||
usesReference,
|
||||
isUsingReferencePreviewMode,
|
||||
} );
|
||||
|
||||
const blockProps = useBlockProps();
|
||||
|
|
|
@ -9,6 +9,16 @@ import { type AttributeMetadata } from '@woocommerce/types';
|
|||
*/
|
||||
import { WooCommerceBlockLocation } from '../product-template/utils';
|
||||
|
||||
export enum ProductCollectionUIStatesInEditor {
|
||||
COLLECTION_PICKER = 'collection_chooser',
|
||||
PRODUCT_REFERENCE_PICKER = 'product_context_picker',
|
||||
VALID_WITH_PREVIEW = 'uses_reference_preview_mode',
|
||||
VALID = 'valid',
|
||||
// Future states
|
||||
// INVALID = 'invalid',
|
||||
// DELETED_PRODUCT_REFERENCE = 'deleted_product_reference',
|
||||
}
|
||||
|
||||
export interface ProductCollectionAttributes {
|
||||
query: ProductCollectionQuery;
|
||||
queryId: number;
|
||||
|
@ -95,6 +105,7 @@ export interface ProductCollectionQuery {
|
|||
woocommerceHandPickedProducts: string[];
|
||||
priceRange: undefined | PriceRange;
|
||||
filterable: boolean;
|
||||
productReference?: number;
|
||||
}
|
||||
|
||||
export type ProductCollectionEditComponentProps =
|
||||
|
@ -108,6 +119,8 @@ export type ProductCollectionEditComponentProps =
|
|||
context: {
|
||||
templateSlug: string;
|
||||
};
|
||||
isUsingReferencePreviewMode: boolean;
|
||||
location: WooCommerceBlockLocation;
|
||||
};
|
||||
|
||||
export type TProductCollectionOrder = 'asc' | 'desc';
|
||||
|
|
|
@ -6,8 +6,10 @@ import { addFilter } from '@wordpress/hooks';
|
|||
import { select } from '@wordpress/data';
|
||||
import { isWpVersion } from '@woocommerce/settings';
|
||||
import type { BlockEditProps, Block } from '@wordpress/blocks';
|
||||
import { useLayoutEffect } from '@wordpress/element';
|
||||
import { useEffect, useLayoutEffect, useState } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import type { ProductResponseItem } from '@woocommerce/types';
|
||||
import { getProduct } from '@woocommerce/editor-components/utils';
|
||||
import {
|
||||
createBlock,
|
||||
// @ts-expect-error Type definitions for this function are missing in Guteberg
|
||||
|
@ -18,13 +20,14 @@ import {
|
|||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
type ProductCollectionAttributes,
|
||||
type TProductCollectionOrder,
|
||||
type TProductCollectionOrderBy,
|
||||
type ProductCollectionQuery,
|
||||
type ProductCollectionDisplayLayout,
|
||||
type PreviewState,
|
||||
type SetPreviewState,
|
||||
ProductCollectionAttributes,
|
||||
TProductCollectionOrder,
|
||||
TProductCollectionOrderBy,
|
||||
ProductCollectionQuery,
|
||||
ProductCollectionDisplayLayout,
|
||||
PreviewState,
|
||||
SetPreviewState,
|
||||
ProductCollectionUIStatesInEditor,
|
||||
} from './types';
|
||||
import {
|
||||
coreQueryPaginationBlockName,
|
||||
|
@ -166,41 +169,14 @@ export const addProductCollectionToQueryPaginationParentOrAncestor = () => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Get the preview message for the Product Collection block based on the usesReference.
|
||||
* There are two scenarios:
|
||||
* 1. When usesReference is product, the preview message will be:
|
||||
* "Actual products will vary depending on the product being viewed."
|
||||
* 2. For all other usesReference, the preview message will be:
|
||||
* "Actual products will vary depending on the page being viewed."
|
||||
*
|
||||
* This message will be shown when the usesReference isn't available on the Editor side, but is available on the Frontend.
|
||||
* Get the message to show in the preview label when the block is in preview mode based
|
||||
* on the `usesReference` value.
|
||||
*/
|
||||
export const getUsesReferencePreviewMessage = (
|
||||
location: WooCommerceBlockLocation,
|
||||
usesReference?: string[]
|
||||
isUsingReferencePreviewMode: boolean
|
||||
) => {
|
||||
if ( ! ( Array.isArray( usesReference ) && usesReference.length > 0 ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( usesReference.includes( location.type ) ) {
|
||||
/**
|
||||
* Block shouldn't be in preview mode when:
|
||||
* 1. Current location is archive and termId is available.
|
||||
* 2. Current location is product and productId is available.
|
||||
*
|
||||
* Because in these cases, we have required context on the editor side.
|
||||
*/
|
||||
const isArchiveLocationWithTermId =
|
||||
location.type === LocationType.Archive &&
|
||||
( location.sourceData?.termId ?? null ) !== null;
|
||||
const isProductLocationWithProductId =
|
||||
location.type === LocationType.Product &&
|
||||
( location.sourceData?.productId ?? null ) !== null;
|
||||
if ( isArchiveLocationWithTermId || isProductLocationWithProductId ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( isUsingReferencePreviewMode ) {
|
||||
if ( location.type === LocationType.Product ) {
|
||||
return __(
|
||||
'Actual products will vary depending on the product being viewed.',
|
||||
|
@ -217,12 +193,77 @@ export const getUsesReferencePreviewMessage = (
|
|||
return '';
|
||||
};
|
||||
|
||||
export const getProductCollectionUIStateInEditor = ( {
|
||||
location,
|
||||
usesReference,
|
||||
attributes,
|
||||
hasInnerBlocks,
|
||||
}: {
|
||||
location: WooCommerceBlockLocation;
|
||||
usesReference?: string[] | undefined;
|
||||
attributes: ProductCollectionAttributes;
|
||||
hasInnerBlocks: boolean;
|
||||
} ): ProductCollectionUIStatesInEditor => {
|
||||
const isInRequiredLocation = usesReference?.includes( location.type );
|
||||
const isCollectionSelected = !! attributes.collection;
|
||||
|
||||
/**
|
||||
* Case 1: Product context picker
|
||||
*/
|
||||
const isProductContextRequired = usesReference?.includes( 'product' );
|
||||
const isProductContextSelected =
|
||||
( attributes.query?.productReference ?? null ) !== null;
|
||||
if (
|
||||
isCollectionSelected &&
|
||||
isProductContextRequired &&
|
||||
! isInRequiredLocation &&
|
||||
! isProductContextSelected
|
||||
) {
|
||||
return ProductCollectionUIStatesInEditor.PRODUCT_REFERENCE_PICKER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Case 2: Preview mode - based on `usesReference` value
|
||||
*/
|
||||
if ( isInRequiredLocation ) {
|
||||
/**
|
||||
* Block shouldn't be in preview mode when:
|
||||
* 1. Current location is archive and termId is available.
|
||||
* 2. Current location is product and productId is available.
|
||||
*
|
||||
* Because in these cases, we have required context on the editor side.
|
||||
*/
|
||||
const isArchiveLocationWithTermId =
|
||||
location.type === LocationType.Archive &&
|
||||
( location.sourceData?.termId ?? null ) !== null;
|
||||
const isProductLocationWithProductId =
|
||||
location.type === LocationType.Product &&
|
||||
( location.sourceData?.productId ?? null ) !== null;
|
||||
|
||||
if (
|
||||
! isArchiveLocationWithTermId &&
|
||||
! isProductLocationWithProductId
|
||||
) {
|
||||
return ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Case 3: Collection chooser
|
||||
*/
|
||||
if ( ! hasInnerBlocks && ! isCollectionSelected ) {
|
||||
return ProductCollectionUIStatesInEditor.COLLECTION_PICKER;
|
||||
}
|
||||
|
||||
return ProductCollectionUIStatesInEditor.VALID;
|
||||
};
|
||||
|
||||
export const useSetPreviewState = ( {
|
||||
setPreviewState,
|
||||
location,
|
||||
attributes,
|
||||
setAttributes,
|
||||
usesReference,
|
||||
isUsingReferencePreviewMode,
|
||||
}: {
|
||||
setPreviewState?: SetPreviewState | undefined;
|
||||
location: WooCommerceBlockLocation;
|
||||
|
@ -231,6 +272,7 @@ export const useSetPreviewState = ( {
|
|||
attributes: Partial< ProductCollectionAttributes >
|
||||
) => void;
|
||||
usesReference?: string[] | undefined;
|
||||
isUsingReferencePreviewMode: boolean;
|
||||
} ) => {
|
||||
const setState = ( newPreviewState: PreviewState ) => {
|
||||
setAttributes( {
|
||||
|
@ -240,8 +282,6 @@ export const useSetPreviewState = ( {
|
|||
},
|
||||
} );
|
||||
};
|
||||
const isCollectionUsesReference =
|
||||
usesReference && usesReference?.length > 0;
|
||||
|
||||
/**
|
||||
* When usesReference is available on Frontend but not on Editor side,
|
||||
|
@ -249,10 +289,10 @@ export const useSetPreviewState = ( {
|
|||
*/
|
||||
const usesReferencePreviewMessage = getUsesReferencePreviewMessage(
|
||||
location,
|
||||
usesReference
|
||||
isUsingReferencePreviewMode
|
||||
);
|
||||
useLayoutEffect( () => {
|
||||
if ( isCollectionUsesReference ) {
|
||||
if ( isUsingReferencePreviewMode ) {
|
||||
setAttributes( {
|
||||
__privatePreviewState: {
|
||||
isPreview: usesReferencePreviewMessage.length > 0,
|
||||
|
@ -263,12 +303,12 @@ export const useSetPreviewState = ( {
|
|||
}, [
|
||||
setAttributes,
|
||||
usesReferencePreviewMessage,
|
||||
isCollectionUsesReference,
|
||||
isUsingReferencePreviewMode,
|
||||
] );
|
||||
|
||||
// Running setPreviewState function provided by Collection, if it exists.
|
||||
useLayoutEffect( () => {
|
||||
if ( ! setPreviewState && ! isCollectionUsesReference ) {
|
||||
if ( ! setPreviewState && ! isUsingReferencePreviewMode ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -294,11 +334,14 @@ export const useSetPreviewState = ( {
|
|||
* - Products by tag
|
||||
* - Products by attribute
|
||||
*/
|
||||
const termId =
|
||||
location.type === LocationType.Archive
|
||||
? location.sourceData?.termId
|
||||
: null;
|
||||
useLayoutEffect( () => {
|
||||
if ( ! setPreviewState && ! isCollectionUsesReference ) {
|
||||
if ( ! setPreviewState && ! isUsingReferencePreviewMode ) {
|
||||
const isGenericArchiveTemplate =
|
||||
location.type === LocationType.Archive &&
|
||||
location.sourceData?.termId === null;
|
||||
location.type === LocationType.Archive && termId === null;
|
||||
|
||||
setAttributes( {
|
||||
__privatePreviewState: {
|
||||
|
@ -315,11 +358,11 @@ export const useSetPreviewState = ( {
|
|||
}, [
|
||||
attributes?.query?.inherit,
|
||||
usesReferencePreviewMessage,
|
||||
location.sourceData?.termId,
|
||||
termId,
|
||||
location.type,
|
||||
setAttributes,
|
||||
setPreviewState,
|
||||
isCollectionUsesReference,
|
||||
isUsingReferencePreviewMode,
|
||||
] );
|
||||
};
|
||||
|
||||
|
@ -356,3 +399,35 @@ export const getDefaultProductCollection = () =>
|
|||
},
|
||||
createBlocksFromInnerBlocksTemplate( INNER_BLOCKS_TEMPLATE )
|
||||
);
|
||||
|
||||
export const useGetProduct = ( productId: number | undefined ) => {
|
||||
const [ product, setProduct ] = useState< ProductResponseItem | null >(
|
||||
null
|
||||
);
|
||||
const [ isLoading, setIsLoading ] = useState< boolean >( false );
|
||||
|
||||
useEffect( () => {
|
||||
const fetchProduct = async () => {
|
||||
if ( productId ) {
|
||||
setIsLoading( true );
|
||||
try {
|
||||
const fetchedProduct = ( await getProduct(
|
||||
productId
|
||||
) ) as ProductResponseItem;
|
||||
setProduct( fetchedProduct );
|
||||
} catch ( error ) {
|
||||
setProduct( null );
|
||||
} finally {
|
||||
setIsLoading( false );
|
||||
}
|
||||
} else {
|
||||
setProduct( null );
|
||||
setIsLoading( false );
|
||||
}
|
||||
};
|
||||
|
||||
fetchProduct();
|
||||
}, [ productId ] );
|
||||
|
||||
return { product, isLoading };
|
||||
};
|
||||
|
|
|
@ -266,7 +266,7 @@ const ProductTemplateEdit = (
|
|||
products: getEntityRecords( 'postType', postType, {
|
||||
...query,
|
||||
...restQueryArgs,
|
||||
location,
|
||||
productCollectionLocation: location,
|
||||
productCollectionQueryContext,
|
||||
previewState: __privateProductCollectionPreviewState,
|
||||
/**
|
||||
|
|
|
@ -63,17 +63,65 @@ const prepareIsInGenericTemplate =
|
|||
( entitySlug: string ): boolean =>
|
||||
templateSlug === entitySlug;
|
||||
|
||||
export type WooCommerceBlockLocation = ReturnType<
|
||||
typeof createLocationObject
|
||||
>;
|
||||
interface WooCommerceBaseLocation {
|
||||
type: LocationType;
|
||||
sourceData?: object | undefined;
|
||||
}
|
||||
|
||||
const createLocationObject = (
|
||||
type: LocationType,
|
||||
sourceData: Record< string, unknown > = {}
|
||||
) => ( {
|
||||
interface ProductLocation extends WooCommerceBaseLocation {
|
||||
type: LocationType.Product;
|
||||
sourceData?:
|
||||
| {
|
||||
productId: number;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
|
||||
interface ArchiveLocation extends WooCommerceBaseLocation {
|
||||
type: LocationType.Archive;
|
||||
sourceData?:
|
||||
| {
|
||||
taxonomy: string;
|
||||
termId: number;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
|
||||
interface CartLocation extends WooCommerceBaseLocation {
|
||||
type: LocationType.Cart;
|
||||
sourceData?:
|
||||
| {
|
||||
productIds: number[];
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
|
||||
interface OrderLocation extends WooCommerceBaseLocation {
|
||||
type: LocationType.Order;
|
||||
sourceData?:
|
||||
| {
|
||||
orderId: number;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
|
||||
interface SiteLocation extends WooCommerceBaseLocation {
|
||||
type: LocationType.Site;
|
||||
sourceData?: object | undefined;
|
||||
}
|
||||
|
||||
export type WooCommerceBlockLocation =
|
||||
| ProductLocation
|
||||
| ArchiveLocation
|
||||
| CartLocation
|
||||
| OrderLocation
|
||||
| SiteLocation;
|
||||
|
||||
const createLocationObject = ( type: LocationType, sourceData: object = {} ) =>
|
||||
( {
|
||||
type,
|
||||
sourceData,
|
||||
} );
|
||||
} as WooCommerceBlockLocation );
|
||||
|
||||
type ContextProperties = {
|
||||
templateSlug: string;
|
||||
|
@ -83,7 +131,7 @@ type ContextProperties = {
|
|||
export const useGetLocation = < T, >(
|
||||
context: Context< T & ContextProperties >,
|
||||
clientId: string
|
||||
) => {
|
||||
): WooCommerceBlockLocation => {
|
||||
const templateSlug = context.templateSlug || '';
|
||||
const postId = context.postId || null;
|
||||
|
||||
|
|
|
@ -62,6 +62,16 @@ interface ProductControlProps {
|
|||
* Whether to show variations in the list of items available.
|
||||
*/
|
||||
showVariations?: boolean;
|
||||
/**
|
||||
* Different messages to display in the component.
|
||||
* If any of the messages are not provided, the default message will be used.
|
||||
*/
|
||||
messages?: {
|
||||
list?: string;
|
||||
noItems?: string;
|
||||
search?: string;
|
||||
updated?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const messages = {
|
||||
|
@ -188,7 +198,7 @@ const ProductControl = (
|
|||
} else if ( showVariations ) {
|
||||
return renderItemWithVariations;
|
||||
}
|
||||
return () => null;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
if ( error ) {
|
||||
|
@ -216,7 +226,10 @@ const ProductControl = (
|
|||
onChange={ onChange }
|
||||
renderItem={ getRenderItemFunc() }
|
||||
onSearch={ onSearch }
|
||||
messages={ messages }
|
||||
messages={ {
|
||||
...messages,
|
||||
...props.messages,
|
||||
} }
|
||||
isHierarchical
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -9,6 +9,7 @@ import { test as base, expect } from '@woocommerce/e2e-utils';
|
|||
*/
|
||||
import ProductCollectionPage, {
|
||||
BLOCK_LABELS,
|
||||
Collections,
|
||||
SELECTORS,
|
||||
} from './product-collection.page';
|
||||
|
||||
|
@ -402,7 +403,7 @@ test.describe( 'Product Collection', () => {
|
|||
} );
|
||||
} );
|
||||
|
||||
test.describe( 'Location is recognised', () => {
|
||||
test.describe( 'Location is recognized', () => {
|
||||
const filterRequest = ( request: Request ) => {
|
||||
const url = request.url();
|
||||
return (
|
||||
|
@ -418,7 +419,9 @@ test.describe( 'Product Collection', () => {
|
|||
return (
|
||||
url.includes( 'wp/v2/product' ) &&
|
||||
searchParams.get( 'isProductCollectionBlock' ) === 'true' &&
|
||||
!! searchParams.get( `location[sourceData][productId]` )
|
||||
!! searchParams.get(
|
||||
`productCollectionLocation[sourceData][productId]`
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -430,26 +433,30 @@ test.describe( 'Product Collection', () => {
|
|||
|
||||
if ( locationType === 'product' ) {
|
||||
return {
|
||||
type: searchParams.get( 'location[type]' ),
|
||||
type: searchParams.get( 'productCollectionLocation[type]' ),
|
||||
productId: searchParams.get(
|
||||
`location[sourceData][productId]`
|
||||
`productCollectionLocation[sourceData][productId]`
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if ( locationType === 'archive' ) {
|
||||
return {
|
||||
type: searchParams.get( 'location[type]' ),
|
||||
type: searchParams.get( 'productCollectionLocation[type]' ),
|
||||
taxonomy: searchParams.get(
|
||||
`location[sourceData][taxonomy]`
|
||||
`productCollectionLocation[sourceData][taxonomy]`
|
||||
),
|
||||
termId: searchParams.get(
|
||||
`productCollectionLocation[sourceData][termId]`
|
||||
),
|
||||
termId: searchParams.get( `location[sourceData][termId]` ),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: searchParams.get( 'location[type]' ),
|
||||
sourceData: searchParams.get( `location[sourceData]` ),
|
||||
type: searchParams.get( 'productCollectionLocation[type]' ),
|
||||
sourceData: searchParams.get(
|
||||
`productCollectionLocation[sourceData]`
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -482,10 +489,10 @@ test.describe( 'Product Collection', () => {
|
|||
pageObject.BLOCK_NAME
|
||||
);
|
||||
|
||||
const locationReuqestPromise =
|
||||
const locationRequestPromise =
|
||||
page.waitForRequest( filterProductRequest );
|
||||
await pageObject.chooseCollectionInTemplate( 'featured' );
|
||||
const locationRequest = await locationReuqestPromise;
|
||||
const locationRequest = await locationRequestPromise;
|
||||
|
||||
const { type, productId } = getLocationDetailsFromRequest(
|
||||
locationRequest,
|
||||
|
@ -961,3 +968,309 @@ test.describe( 'Product Collection', () => {
|
|||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
test.describe( 'Testing "usesReference" argument in "registerProductCollection"', () => {
|
||||
const MY_REGISTERED_COLLECTIONS = {
|
||||
myCustomCollectionWithProductContext: {
|
||||
name: 'My Custom Collection - Product Context',
|
||||
label: 'Block: My Custom Collection - Product Context',
|
||||
previewLabelTemplate: [ 'woocommerce/woocommerce//single-product' ],
|
||||
shouldShowProductPicker: true,
|
||||
},
|
||||
myCustomCollectionWithCartContext: {
|
||||
name: 'My Custom Collection - Cart Context',
|
||||
label: 'Block: My Custom Collection - Cart Context',
|
||||
previewLabelTemplate: [ 'woocommerce/woocommerce//page-cart' ],
|
||||
shouldShowProductPicker: false,
|
||||
},
|
||||
myCustomCollectionWithOrderContext: {
|
||||
name: 'My Custom Collection - Order Context',
|
||||
label: 'Block: My Custom Collection - Order Context',
|
||||
previewLabelTemplate: [
|
||||
'woocommerce/woocommerce//order-confirmation',
|
||||
],
|
||||
shouldShowProductPicker: false,
|
||||
},
|
||||
myCustomCollectionWithArchiveContext: {
|
||||
name: 'My Custom Collection - Archive Context',
|
||||
label: 'Block: My Custom Collection - Archive Context',
|
||||
previewLabelTemplate: [
|
||||
'woocommerce/woocommerce//taxonomy-product_cat',
|
||||
],
|
||||
shouldShowProductPicker: false,
|
||||
},
|
||||
myCustomCollectionMultipleContexts: {
|
||||
name: 'My Custom Collection - Multiple Contexts',
|
||||
label: 'Block: My Custom Collection - Multiple Contexts',
|
||||
previewLabelTemplate: [
|
||||
'woocommerce/woocommerce//single-product',
|
||||
'woocommerce/woocommerce//order-confirmation',
|
||||
],
|
||||
shouldShowProductPicker: true,
|
||||
},
|
||||
};
|
||||
|
||||
// Activate plugin which registers custom product collections
|
||||
test.beforeEach( async ( { requestUtils } ) => {
|
||||
await requestUtils.activatePlugin(
|
||||
'register-product-collection-tester'
|
||||
);
|
||||
} );
|
||||
|
||||
Object.entries( MY_REGISTERED_COLLECTIONS ).forEach(
|
||||
( [ key, collection ] ) => {
|
||||
for ( const template of collection.previewLabelTemplate ) {
|
||||
test( `Collection "${ collection.name }" should show preview label in "${ template }"`, async ( {
|
||||
pageObject,
|
||||
editor,
|
||||
} ) => {
|
||||
await pageObject.goToEditorTemplate( template );
|
||||
await pageObject.insertProductCollection();
|
||||
await pageObject.chooseCollectionInTemplate(
|
||||
key as Collections
|
||||
);
|
||||
|
||||
const block = editor.canvas.getByLabel( collection.label );
|
||||
const previewButtonLocator = block.getByTestId(
|
||||
SELECTORS.previewButtonTestID
|
||||
);
|
||||
|
||||
await expect( previewButtonLocator ).toBeVisible();
|
||||
} );
|
||||
}
|
||||
|
||||
test( `Collection "${ collection.name }" should not show preview label in a post`, async ( {
|
||||
pageObject,
|
||||
editor,
|
||||
admin,
|
||||
} ) => {
|
||||
await admin.createNewPost();
|
||||
await pageObject.insertProductCollection();
|
||||
await pageObject.chooseCollectionInPost( key as Collections );
|
||||
|
||||
// Check visibility of product picker
|
||||
const editorProductPicker = editor.canvas.locator(
|
||||
SELECTORS.productPicker
|
||||
);
|
||||
const expectedVisibility = collection.shouldShowProductPicker
|
||||
? 'toBeVisible'
|
||||
: 'toBeHidden';
|
||||
await expect( editorProductPicker )[ expectedVisibility ]();
|
||||
|
||||
if ( collection.shouldShowProductPicker ) {
|
||||
await pageObject.chooseProductInEditorProductPickerIfAvailable(
|
||||
editor.canvas
|
||||
);
|
||||
}
|
||||
|
||||
// At this point, the product picker should be hidden
|
||||
await expect( editorProductPicker ).toBeHidden();
|
||||
|
||||
// Check visibility of preview label
|
||||
const block = editor.canvas.getByLabel( collection.label );
|
||||
const previewButtonLocator = block.getByTestId(
|
||||
SELECTORS.previewButtonTestID
|
||||
);
|
||||
|
||||
await expect( previewButtonLocator ).toBeHidden();
|
||||
} );
|
||||
|
||||
test( `Collection "${ collection.name }" should not show preview label in Product Catalog template`, async ( {
|
||||
pageObject,
|
||||
editor,
|
||||
} ) => {
|
||||
await pageObject.goToProductCatalogAndInsertCollection(
|
||||
key as Collections
|
||||
);
|
||||
|
||||
const block = editor.canvas.getByLabel( collection.label );
|
||||
const previewButtonLocator = block.getByTestId(
|
||||
SELECTORS.previewButtonTestID
|
||||
);
|
||||
|
||||
await expect( previewButtonLocator ).toBeHidden();
|
||||
} );
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
test.describe( 'Product picker', () => {
|
||||
const MY_REGISTERED_COLLECTIONS_THAT_NEEDS_PRODUCT = {
|
||||
myCustomCollectionWithProductContext: {
|
||||
name: 'My Custom Collection - Product Context',
|
||||
label: 'Block: My Custom Collection - Product Context',
|
||||
collection:
|
||||
'woocommerce/product-collection/my-custom-collection-product-context',
|
||||
},
|
||||
myCustomCollectionMultipleContexts: {
|
||||
name: 'My Custom Collection - Multiple Contexts',
|
||||
label: 'Block: My Custom Collection - Multiple Contexts',
|
||||
collection:
|
||||
'woocommerce/product-collection/my-custom-collection-multiple-contexts',
|
||||
},
|
||||
};
|
||||
|
||||
// Activate plugin which registers custom product collections
|
||||
test.beforeEach( async ( { requestUtils } ) => {
|
||||
await requestUtils.activatePlugin(
|
||||
'register-product-collection-tester'
|
||||
);
|
||||
} );
|
||||
|
||||
Object.entries( MY_REGISTERED_COLLECTIONS_THAT_NEEDS_PRODUCT ).forEach(
|
||||
( [ key, collection ] ) => {
|
||||
test( `For collection "${ collection.name }" - manually selected product reference should be available on Frontend in a post`, async ( {
|
||||
pageObject,
|
||||
admin,
|
||||
page,
|
||||
editor,
|
||||
} ) => {
|
||||
await admin.createNewPost();
|
||||
await pageObject.insertProductCollection();
|
||||
await pageObject.chooseCollectionInPost( key as Collections );
|
||||
|
||||
// Verify that product picker is shown in Editor
|
||||
const editorProductPicker = editor.canvas.locator(
|
||||
SELECTORS.productPicker
|
||||
);
|
||||
await expect( editorProductPicker ).toBeVisible();
|
||||
|
||||
// Once a product is selected, the product picker should be hidden
|
||||
await pageObject.chooseProductInEditorProductPickerIfAvailable(
|
||||
editor.canvas
|
||||
);
|
||||
await expect( editorProductPicker ).toBeHidden();
|
||||
|
||||
// On Frontend, verify that product reference is a number
|
||||
await pageObject.publishAndGoToFrontend();
|
||||
const collectionWithProductContext = page.locator(
|
||||
`[data-collection="${ collection.collection }"]`
|
||||
);
|
||||
const queryAttribute = JSON.parse(
|
||||
( await collectionWithProductContext.getAttribute(
|
||||
'data-query'
|
||||
) ) || '{}'
|
||||
);
|
||||
expect( typeof queryAttribute?.productReference ).toBe(
|
||||
'number'
|
||||
);
|
||||
} );
|
||||
|
||||
test( `For collection "${ collection.name }" - changing product using inspector control`, async ( {
|
||||
pageObject,
|
||||
admin,
|
||||
page,
|
||||
editor,
|
||||
} ) => {
|
||||
await admin.createNewPost();
|
||||
await pageObject.insertProductCollection();
|
||||
await pageObject.chooseCollectionInPost( key as Collections );
|
||||
|
||||
// Verify that product picker is shown in Editor
|
||||
const editorProductPicker = editor.canvas.locator(
|
||||
SELECTORS.productPicker
|
||||
);
|
||||
await expect( editorProductPicker ).toBeVisible();
|
||||
|
||||
// Once a product is selected, the product picker should be hidden
|
||||
await pageObject.chooseProductInEditorProductPickerIfAvailable(
|
||||
editor.canvas
|
||||
);
|
||||
await expect( editorProductPicker ).toBeHidden();
|
||||
|
||||
// Verify that Album is selected
|
||||
await expect(
|
||||
admin.page.locator( SELECTORS.linkedProductControl.button )
|
||||
).toContainText( 'Album' );
|
||||
|
||||
// Change product using inspector control to Beanie
|
||||
await admin.page
|
||||
.locator( SELECTORS.linkedProductControl.button )
|
||||
.click();
|
||||
await admin.page
|
||||
.locator( SELECTORS.linkedProductControl.popoverContent )
|
||||
.getByLabel( 'Beanie', { exact: true } )
|
||||
.click();
|
||||
await expect(
|
||||
admin.page.locator( SELECTORS.linkedProductControl.button )
|
||||
).toContainText( 'Beanie' );
|
||||
|
||||
// On Frontend, verify that product reference is a number
|
||||
await pageObject.publishAndGoToFrontend();
|
||||
const collectionWithProductContext = page.locator(
|
||||
`[data-collection="${ collection.collection }"]`
|
||||
);
|
||||
const queryAttribute = JSON.parse(
|
||||
( await collectionWithProductContext.getAttribute(
|
||||
'data-query'
|
||||
) ) || '{}'
|
||||
);
|
||||
expect( typeof queryAttribute?.productReference ).toBe(
|
||||
'number'
|
||||
);
|
||||
} );
|
||||
|
||||
test( `For collection "${ collection.name }" - product picker shouldn't be shown in Single Product template`, async ( {
|
||||
pageObject,
|
||||
admin,
|
||||
editor,
|
||||
} ) => {
|
||||
await admin.visitSiteEditor( {
|
||||
postId: `woocommerce/woocommerce//single-product`,
|
||||
postType: 'wp_template',
|
||||
canvas: 'edit',
|
||||
} );
|
||||
await editor.canvas.locator( 'body' ).click();
|
||||
await pageObject.insertProductCollection();
|
||||
await pageObject.chooseCollectionInTemplate(
|
||||
key as Collections
|
||||
);
|
||||
|
||||
const editorProductPicker = editor.canvas.locator(
|
||||
SELECTORS.productPicker
|
||||
);
|
||||
await expect( editorProductPicker ).toBeHidden();
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
test( 'Product picker should work as expected while changing collection using "Choose collection" button from Toolbar', async ( {
|
||||
pageObject,
|
||||
admin,
|
||||
editor,
|
||||
} ) => {
|
||||
await admin.createNewPost();
|
||||
await pageObject.insertProductCollection();
|
||||
await pageObject.chooseCollectionInPost(
|
||||
'myCustomCollectionWithProductContext'
|
||||
);
|
||||
|
||||
// Verify that product picker is shown in Editor
|
||||
const editorProductPicker = editor.canvas.locator(
|
||||
SELECTORS.productPicker
|
||||
);
|
||||
await expect( editorProductPicker ).toBeVisible();
|
||||
|
||||
// Once a product is selected, the product picker should be hidden
|
||||
await pageObject.chooseProductInEditorProductPickerIfAvailable(
|
||||
editor.canvas
|
||||
);
|
||||
await expect( editorProductPicker ).toBeHidden();
|
||||
|
||||
// Change collection using Toolbar
|
||||
await pageObject.changeCollectionUsingToolbar(
|
||||
'myCustomCollectionMultipleContexts'
|
||||
);
|
||||
await expect( editorProductPicker ).toBeVisible();
|
||||
|
||||
// Once a product is selected, the product picker should be hidden
|
||||
await pageObject.chooseProductInEditorProductPickerIfAvailable(
|
||||
editor.canvas
|
||||
);
|
||||
await expect( editorProductPicker ).toBeHidden();
|
||||
|
||||
// Product picker should be hidden for collections that don't need product
|
||||
await pageObject.changeCollectionUsingToolbar( 'featured' );
|
||||
await expect( editorProductPicker ).toBeHidden();
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Locator, Page } from '@playwright/test';
|
||||
import { FrameLocator, Locator, Page } from '@playwright/test';
|
||||
import { Editor, Admin } from '@woocommerce/e2e-utils';
|
||||
import { BlockRepresentation } from '@wordpress/e2e-test-utils-playwright/build-types/editor/insert-block';
|
||||
|
||||
|
@ -62,6 +62,12 @@ export const SELECTORS = {
|
|||
previewButtonTestID: 'product-collection-preview-button',
|
||||
collectionPlaceholder:
|
||||
'[data-type="woocommerce/product-collection"] .components-placeholder',
|
||||
productPicker: '.wc-blocks-product-collection__editor-product-picker',
|
||||
linkedProductControl: {
|
||||
button: '.wc-block-product-collection-linked-product-control__button',
|
||||
popoverContent:
|
||||
'.wc-block-product-collection-linked-product__popover-content',
|
||||
},
|
||||
};
|
||||
|
||||
export type Collections =
|
||||
|
@ -200,10 +206,31 @@ class ProductCollectionPage {
|
|||
}
|
||||
}
|
||||
|
||||
async chooseProductInEditorProductPickerIfAvailable(
|
||||
pageReference: Page | FrameLocator
|
||||
) {
|
||||
const editorProductPicker = pageReference.locator(
|
||||
SELECTORS.productPicker
|
||||
);
|
||||
|
||||
if ( await editorProductPicker.isVisible() ) {
|
||||
await editorProductPicker
|
||||
.locator( 'label' )
|
||||
.filter( {
|
||||
hasText: 'Album',
|
||||
} )
|
||||
.click();
|
||||
}
|
||||
}
|
||||
|
||||
async createNewPostAndInsertBlock( collection?: Collections ) {
|
||||
await this.admin.createNewPost();
|
||||
await this.insertProductCollection();
|
||||
await this.chooseCollectionInPost( collection );
|
||||
// If product picker is available, choose a product.
|
||||
await this.chooseProductInEditorProductPickerIfAvailable(
|
||||
this.admin.page
|
||||
);
|
||||
await this.refreshLocators( 'editor' );
|
||||
await this.editor.openDocumentSettingsSidebar();
|
||||
}
|
||||
|
@ -345,6 +372,10 @@ class ProductCollectionPage {
|
|||
await this.editor.canvas.locator( 'body' ).click();
|
||||
await this.insertProductCollection();
|
||||
await this.chooseCollectionInTemplate( collection );
|
||||
// If product picker is available, choose a product.
|
||||
await this.chooseProductInEditorProductPickerIfAvailable(
|
||||
this.editor.canvas
|
||||
);
|
||||
await this.refreshLocators( 'editor' );
|
||||
}
|
||||
|
||||
|
@ -571,6 +602,30 @@ class ProductCollectionPage {
|
|||
.click();
|
||||
}
|
||||
|
||||
async changeCollectionUsingToolbar( collection: Collections ) {
|
||||
// Click "Choose collection" button in the toolbar.
|
||||
await this.admin.page
|
||||
.getByRole( 'toolbar', { name: 'Block Tools' } )
|
||||
.getByRole( 'button', { name: 'Choose collection' } )
|
||||
.click();
|
||||
|
||||
// Select the collection from the modal.
|
||||
const collectionChooserModal = this.admin.page.locator(
|
||||
'.wc-blocks-product-collection__modal'
|
||||
);
|
||||
await collectionChooserModal
|
||||
.getByRole( 'button', {
|
||||
name: collectionToButtonNameMap[ collection ],
|
||||
} )
|
||||
.click();
|
||||
|
||||
await collectionChooserModal
|
||||
.getByRole( 'button', {
|
||||
name: 'Continue',
|
||||
} )
|
||||
.click();
|
||||
}
|
||||
|
||||
async setDisplaySettings( {
|
||||
itemsPerPage,
|
||||
offset,
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: major
|
||||
Type: add
|
||||
|
||||
Product Collection - Show product picker in Editor when collection requires a product but not available <details> A collection can define if it requires a product context. This can be done using `usesReference` argument i.e. ```tsx __experimentalRegisterProductCollection({ ..., usesReference: ['product'], ) ``` When product context doesn't exist in current template/page/post etc. then we show product picker in Editor. This way, merchant can manually provide a product context to the collection.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: major
|
||||
Type: add
|
||||
|
||||
Product Collection - Implement Inspector control to change selected product
|
Loading…
Reference in New Issue