Merge branch 'trunk' into fix/43611-product-variation-clear-button

This commit is contained in:
Sam Seay 2024-09-17 17:04:51 +08:00 committed by GitHub
commit 4a2c5bc1af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 538 additions and 305 deletions

View File

@ -21,15 +21,36 @@ import {
import type { ProductCollectionEditComponentProps } from '../types'; import type { ProductCollectionEditComponentProps } from '../types';
import { getCollectionByName } from '../collections'; import { getCollectionByName } from '../collections';
const ProductPicker = ( props: ProductCollectionEditComponentProps ) => { const ProductPicker = (
props: ProductCollectionEditComponentProps & {
isDeletedProductReference: boolean;
}
) => {
const blockProps = useBlockProps(); const blockProps = useBlockProps();
const attributes = props.attributes; const { attributes, isDeletedProductReference } = props;
const collection = getCollectionByName( attributes.collection ); const collection = getCollectionByName( attributes.collection );
if ( ! collection ) { if ( ! collection ) {
return; return null;
} }
const infoText = isDeletedProductReference
? __(
'Previously selected product is no longer available.',
'woocommerce'
)
: 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 /> }
);
return ( return (
<div { ...blockProps }> <div { ...blockProps }>
<Placeholder className="wc-blocks-product-collection__editor-product-picker"> <Placeholder className="wc-blocks-product-collection__editor-product-picker">
@ -38,21 +59,7 @@ const ProductPicker = ( props: ProductCollectionEditComponentProps ) => {
icon={ info } icon={ info }
className="wc-blocks-product-collection__info-icon" className="wc-blocks-product-collection__info-icon"
/> />
<Text> <Text>{ infoText }</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> </HStack>
<ProductControl <ProductControl
selected={ selected={

View File

@ -174,6 +174,10 @@ $max-button-width: calc(100% / #{$max-button-columns});
.wc-blocks-product-collection__info-icon { .wc-blocks-product-collection__info-icon {
fill: var(--wp--preset--color--luminous-vivid-orange, #e26f56); fill: var(--wp--preset--color--luminous-vivid-orange, #e26f56);
} }
.woocommerce-search-list__search {
margin: 0;
}
} }
// Linked Product Control // Linked Product Control

View File

@ -5,11 +5,13 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
import { useState } from '@wordpress/element'; import { useState } from '@wordpress/element';
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import { useGetLocation } from '@woocommerce/blocks/product-template/utils'; import { useGetLocation } from '@woocommerce/blocks/product-template/utils';
import { Spinner, Flex } from '@wordpress/components';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { import {
ProductCollectionContentProps,
ProductCollectionEditComponentProps, ProductCollectionEditComponentProps,
ProductCollectionUIStatesInEditor, ProductCollectionUIStatesInEditor,
} from '../types'; } from '../types';
@ -17,7 +19,7 @@ import ProductCollectionPlaceholder from './product-collection-placeholder';
import ProductCollectionContent from './product-collection-content'; import ProductCollectionContent from './product-collection-content';
import CollectionSelectionModal from './collection-selection-modal'; import CollectionSelectionModal from './collection-selection-modal';
import './editor.scss'; import './editor.scss';
import { getProductCollectionUIStateInEditor } from '../utils'; import { useProductCollectionUIState } from '../utils';
import ProductPicker from './ProductPicker'; import ProductPicker from './ProductPicker';
const Edit = ( props: ProductCollectionEditComponentProps ) => { const Edit = ( props: ProductCollectionEditComponentProps ) => {
@ -31,49 +33,65 @@ const Edit = ( props: ProductCollectionEditComponentProps ) => {
[ clientId ] [ clientId ]
); );
const productCollectionUIStateInEditor = const { productCollectionUIStateInEditor, isLoading } =
getProductCollectionUIStateInEditor( { useProductCollectionUIState( {
hasInnerBlocks,
location, location,
attributes: props.attributes, attributes,
hasInnerBlocks,
usesReference: props.usesReference, usesReference: props.usesReference,
} ); } );
/** // Show spinner while calculating Editor UI state.
* Component to render based on the UI state. if ( isLoading ) {
*/ return (
let Component, <Flex justify="center" align="center">
isUsingReferencePreviewMode = false; <Spinner />
switch ( productCollectionUIStateInEditor ) { </Flex>
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;
} }
const productCollectionContentProps: ProductCollectionContentProps = {
...props,
openCollectionSelectionModal: () => setIsSelectionModalOpen( true ),
location,
isUsingReferencePreviewMode:
productCollectionUIStateInEditor ===
ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW,
};
const renderComponent = () => {
switch ( productCollectionUIStateInEditor ) {
case ProductCollectionUIStatesInEditor.COLLECTION_PICKER:
return <ProductCollectionPlaceholder { ...props } />;
case ProductCollectionUIStatesInEditor.PRODUCT_REFERENCE_PICKER:
return (
<ProductPicker
{ ...props }
isDeletedProductReference={ false }
/>
);
case ProductCollectionUIStatesInEditor.DELETED_PRODUCT_REFERENCE:
return (
<ProductPicker
{ ...props }
isDeletedProductReference={ true }
/>
);
case ProductCollectionUIStatesInEditor.VALID:
case ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW:
return (
<ProductCollectionContent
{ ...productCollectionContentProps }
/>
);
default:
return <ProductCollectionPlaceholder { ...props } />;
}
};
return ( return (
<> <>
<Component { renderComponent() }
{ ...props }
openCollectionSelectionModal={ () =>
setIsSelectionModalOpen( true )
}
isUsingReferencePreviewMode={ isUsingReferencePreviewMode }
location={ location }
usesReference={ props.usesReference }
/>
{ isSelectionModalOpen && ( { isSelectionModalOpen && (
<CollectionSelectionModal <CollectionSelectionModal
clientId={ clientId } clientId={ clientId }

View File

@ -7,10 +7,10 @@ import { InspectorAdvancedControls } from '@wordpress/block-editor';
* Internal dependencies * Internal dependencies
*/ */
import ForcePageReloadControl from './force-page-reload-control'; import ForcePageReloadControl from './force-page-reload-control';
import type { ProductCollectionEditComponentProps } from '../../types'; import type { ProductCollectionContentProps } from '../../types';
export default function ProductCollectionAdvancedInspectorControls( export default function ProductCollectionAdvancedInspectorControls(
props: Omit< ProductCollectionEditComponentProps, 'preview' > props: ProductCollectionContentProps
) { ) {
const { clientId, attributes, setAttributes } = props; const { clientId, attributes, setAttributes } = props;
const { forcePageReload } = attributes; const { forcePageReload } = attributes;

View File

@ -27,7 +27,7 @@ import {
import metadata from '../../block.json'; import metadata from '../../block.json';
import { useTracksLocation } from '../../tracks-utils'; import { useTracksLocation } from '../../tracks-utils';
import { import {
ProductCollectionEditComponentProps, ProductCollectionContentProps,
ProductCollectionAttributes, ProductCollectionAttributes,
CoreFilterNames, CoreFilterNames,
FilterName, FilterName,
@ -58,7 +58,7 @@ const prepareShouldShowFilter =
}; };
const ProductCollectionInspectorControls = ( const ProductCollectionInspectorControls = (
props: ProductCollectionEditComponentProps props: ProductCollectionContentProps
) => { ) => {
const { attributes, context, setAttributes } = props; const { attributes, context, setAttributes } = props;
const { query, hideControls, displayLayout } = attributes; const { query, hideControls, displayLayout } = attributes;

View File

@ -18,7 +18,7 @@ import fastDeepEqual from 'fast-deep-equal/es6';
import type { import type {
ProductCollectionAttributes, ProductCollectionAttributes,
ProductCollectionQuery, ProductCollectionQuery,
ProductCollectionEditComponentProps, ProductCollectionContentProps,
} from '../types'; } from '../types';
import { DEFAULT_ATTRIBUTES, INNER_BLOCKS_TEMPLATE } from '../constants'; import { DEFAULT_ATTRIBUTES, INNER_BLOCKS_TEMPLATE } from '../constants';
import { import {
@ -68,7 +68,7 @@ const useQueryId = (
const ProductCollectionContent = ( { const ProductCollectionContent = ( {
preview: { setPreviewState, initialPreviewState } = {}, preview: { setPreviewState, initialPreviewState } = {},
...props ...props
}: ProductCollectionEditComponentProps ) => { }: ProductCollectionContentProps ) => {
const isInitialAttributesSet = useRef( false ); const isInitialAttributesSet = useRef( false );
const { const {
clientId, clientId,

View File

@ -11,10 +11,10 @@ import { setQueryAttribute } from '../../utils';
import DisplaySettingsToolbar from './display-settings-toolbar'; import DisplaySettingsToolbar from './display-settings-toolbar';
import DisplayLayoutToolbar from './display-layout-toolbar'; import DisplayLayoutToolbar from './display-layout-toolbar';
import CollectionChooserToolbar from './collection-chooser-toolbar'; import CollectionChooserToolbar from './collection-chooser-toolbar';
import type { ProductCollectionEditComponentProps } from '../../types'; import type { ProductCollectionContentProps } from '../../types';
export default function ToolbarControls( export default function ToolbarControls(
props: Omit< ProductCollectionEditComponentProps, 'preview' > props: ProductCollectionContentProps
) { ) {
const { attributes, openCollectionSelectionModal, setAttributes } = props; const { attributes, openCollectionSelectionModal, setAttributes } = props;
const { query, displayLayout } = attributes; const { query, displayLayout } = attributes;

View File

@ -14,9 +14,9 @@ export enum ProductCollectionUIStatesInEditor {
PRODUCT_REFERENCE_PICKER = 'product_context_picker', PRODUCT_REFERENCE_PICKER = 'product_context_picker',
VALID_WITH_PREVIEW = 'uses_reference_preview_mode', VALID_WITH_PREVIEW = 'uses_reference_preview_mode',
VALID = 'valid', VALID = 'valid',
DELETED_PRODUCT_REFERENCE = 'deleted_product_reference',
// Future states // Future states
// INVALID = 'invalid', // INVALID = 'invalid',
// DELETED_PRODUCT_REFERENCE = 'deleted_product_reference',
} }
export interface ProductCollectionAttributes { export interface ProductCollectionAttributes {
@ -110,7 +110,6 @@ export interface ProductCollectionQuery {
export type ProductCollectionEditComponentProps = export type ProductCollectionEditComponentProps =
BlockEditProps< ProductCollectionAttributes > & { BlockEditProps< ProductCollectionAttributes > & {
openCollectionSelectionModal: () => void;
preview?: { preview?: {
initialPreviewState?: PreviewState; initialPreviewState?: PreviewState;
setPreviewState?: SetPreviewState; setPreviewState?: SetPreviewState;
@ -119,8 +118,13 @@ export type ProductCollectionEditComponentProps =
context: { context: {
templateSlug: string; templateSlug: string;
}; };
isUsingReferencePreviewMode: boolean; };
export type ProductCollectionContentProps =
ProductCollectionEditComponentProps & {
location: WooCommerceBlockLocation; location: WooCommerceBlockLocation;
isUsingReferencePreviewMode: boolean;
openCollectionSelectionModal: () => void;
}; };
export type TProductCollectionOrder = 'asc' | 'desc'; export type TProductCollectionOrder = 'asc' | 'desc';

View File

@ -3,10 +3,16 @@
*/ */
import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as blockEditorStore } from '@wordpress/block-editor';
import { addFilter } from '@wordpress/hooks'; import { addFilter } from '@wordpress/hooks';
import { select } from '@wordpress/data'; import { select, useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
import { isWpVersion } from '@woocommerce/settings'; import { isWpVersion } from '@woocommerce/settings';
import type { BlockEditProps, Block } from '@wordpress/blocks'; import type { BlockEditProps, Block } from '@wordpress/blocks';
import { useEffect, useLayoutEffect, useState } from '@wordpress/element'; import {
useEffect,
useLayoutEffect,
useState,
useMemo,
} from '@wordpress/element';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import type { ProductResponseItem } from '@woocommerce/types'; import type { ProductResponseItem } from '@woocommerce/types';
import { getProduct } from '@woocommerce/editor-components/utils'; import { getProduct } from '@woocommerce/editor-components/utils';
@ -193,7 +199,7 @@ export const getUsesReferencePreviewMessage = (
return ''; return '';
}; };
export const getProductCollectionUIStateInEditor = ( { export const useProductCollectionUIState = ( {
location, location,
usesReference, usesReference,
attributes, attributes,
@ -203,59 +209,111 @@ export const getProductCollectionUIStateInEditor = ( {
usesReference?: string[] | undefined; usesReference?: string[] | undefined;
attributes: ProductCollectionAttributes; attributes: ProductCollectionAttributes;
hasInnerBlocks: boolean; hasInnerBlocks: boolean;
} ): ProductCollectionUIStatesInEditor => { } ) => {
const isInRequiredLocation = usesReference?.includes( location.type ); // Fetch product to check if it's deleted.
const isCollectionSelected = !! attributes.collection; // `product` will be undefined if it doesn't exist.
const productId = attributes.query?.productReference;
const { product, hasResolved } = useSelect(
( selectFunc ) => {
if ( ! productId ) {
return { product: null, hasResolved: true };
}
/** const { getEntityRecord, hasFinishedResolution } =
* Case 1: Product context picker selectFunc( coreDataStore );
*/ const selectorArgs = [ 'postType', 'product', productId ];
const isProductContextRequired = usesReference?.includes( 'product' ); return {
const isProductContextSelected = product: getEntityRecord( ...selectorArgs ),
( attributes.query?.productReference ?? null ) !== null; hasResolved: hasFinishedResolution(
if ( 'getEntityRecord',
isCollectionSelected && selectorArgs
isProductContextRequired && ),
! isInRequiredLocation && };
! isProductContextSelected },
) { [ productId ]
return ProductCollectionUIStatesInEditor.PRODUCT_REFERENCE_PICKER; );
}
const productCollectionUIStateInEditor = useMemo( () => {
const isInRequiredLocation = usesReference?.includes( location.type );
const isCollectionSelected = !! attributes.collection;
/**
* Case 2: Preview mode - based on `usesReference` value
*/
if ( isInRequiredLocation ) {
/** /**
* Block shouldn't be in preview mode when: * Case 1: Product context picker
* 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 = const isProductContextRequired = usesReference?.includes( 'product' );
location.type === LocationType.Archive && const isProductContextSelected =
( location.sourceData?.termId ?? null ) !== null; ( attributes.query?.productReference ?? null ) !== null;
const isProductLocationWithProductId =
location.type === LocationType.Product &&
( location.sourceData?.productId ?? null ) !== null;
if ( if (
! isArchiveLocationWithTermId && isCollectionSelected &&
! isProductLocationWithProductId isProductContextRequired &&
! isInRequiredLocation &&
! isProductContextSelected
) { ) {
return ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW; return ProductCollectionUIStatesInEditor.PRODUCT_REFERENCE_PICKER;
} }
}
/** // Case 2: Deleted product reference
* Case 3: Collection chooser if (
*/ isCollectionSelected &&
if ( ! hasInnerBlocks && ! isCollectionSelected ) { isProductContextRequired &&
return ProductCollectionUIStatesInEditor.COLLECTION_PICKER; ! isInRequiredLocation &&
} isProductContextSelected
) {
const isProductDeleted =
productId &&
( product === undefined || product?.status === 'trash' );
if ( isProductDeleted ) {
return ProductCollectionUIStatesInEditor.DELETED_PRODUCT_REFERENCE;
}
}
return ProductCollectionUIStatesInEditor.VALID; /**
* Case 3: 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 4: Collection chooser
*/
if ( ! hasInnerBlocks && ! isCollectionSelected ) {
return ProductCollectionUIStatesInEditor.COLLECTION_PICKER;
}
return ProductCollectionUIStatesInEditor.VALID;
}, [
location.type,
location.sourceData?.termId,
location.sourceData?.productId,
usesReference,
attributes.collection,
productId,
product,
hasInnerBlocks,
attributes.query?.productReference,
] );
return { productCollectionUIStateInEditor, isLoading: ! hasResolved };
}; };
export const useSetPreviewState = ( { export const useSetPreviewState = ( {

View File

@ -10,6 +10,10 @@ import {
import { BlockEditProps, InnerBlockTemplate } from '@wordpress/blocks'; import { BlockEditProps, InnerBlockTemplate } from '@wordpress/blocks';
import { useEffect } from '@wordpress/element'; import { useEffect } from '@wordpress/element';
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import ErrorPlaceholder, {
ErrorObject,
} from '@woocommerce/editor-components/error-placeholder';
import { __ } from '@wordpress/i18n';
/** /**
* Internal dependencies * Internal dependencies
@ -132,14 +136,16 @@ export const Edit = ( {
useEffect( () => { useEffect( () => {
const mode = getMode( currentTemplateId, templateType ); const mode = getMode( currentTemplateId, templateType );
const newProductGalleryClientId =
attributes.productGalleryClientId || clientId;
setAttributes( { setAttributes( {
...attributes, ...attributes,
mode, mode,
productGalleryClientId: clientId, productGalleryClientId: newProductGalleryClientId,
} ); } );
// Move the Thumbnails block to the correct above or below the Large Image based on the thumbnailsPosition attribute. // Move the Thumbnails block to the correct above or below the Large Image based on the thumbnailsPosition attribute.
moveInnerBlocksToPosition( attributes, clientId ); moveInnerBlocksToPosition( attributes, newProductGalleryClientId );
}, [ }, [
setAttributes, setAttributes,
attributes, attributes,
@ -148,6 +154,18 @@ export const Edit = ( {
templateType, templateType,
] ); ] );
if ( attributes.productGalleryClientId !== clientId ) {
const error = {
message: __(
'productGalleryClientId and clientId codes mismatch.',
'woocommerce'
),
type: 'general',
} as ErrorObject;
return <ErrorPlaceholder error={ error } isLoading={ false } />;
}
return ( return (
<div { ...blockProps }> <div { ...blockProps }>
<InspectorControls> <InspectorControls>

View File

@ -136,10 +136,10 @@ export const moveInnerBlocksToPosition = (
): void => { ): void => {
const { getBlock, getBlockRootClientId, getBlockIndex } = const { getBlock, getBlockRootClientId, getBlockIndex } =
select( 'core/block-editor' ); select( 'core/block-editor' );
const { moveBlockToPosition } = dispatch( 'core/block-editor' );
const productGalleryBlock = getBlock( clientId ); const productGalleryBlock = getBlock( clientId );
if ( productGalleryBlock ) { if ( productGalleryBlock?.name === 'woocommerce/product-gallery' ) {
const { moveBlockToPosition } = dispatch( 'core/block-editor' );
const previousLayout = productGalleryBlock.innerBlocks.length const previousLayout = productGalleryBlock.innerBlocks.length
? productGalleryBlock.innerBlocks[ 0 ].attributes.layout ? productGalleryBlock.innerBlocks[ 0 ].attributes.layout
: null; : null;

View File

@ -207,7 +207,8 @@ class ProductCollectionPage {
} }
async chooseProductInEditorProductPickerIfAvailable( async chooseProductInEditorProductPickerIfAvailable(
pageReference: Page | FrameLocator pageReference: Page | FrameLocator,
productName = 'Album'
) { ) {
const editorProductPicker = pageReference.locator( const editorProductPicker = pageReference.locator(
SELECTORS.productPicker SELECTORS.productPicker
@ -217,7 +218,7 @@ class ProductCollectionPage {
await editorProductPicker await editorProductPicker
.locator( 'label' ) .locator( 'label' )
.filter( { .filter( {
hasText: 'Album', hasText: productName,
} ) } )
.click(); .click();
} }

View File

@ -356,4 +356,84 @@ test.describe( 'Product Collection registration', () => {
await expect( previewButtonLocator ).toBeHidden(); await expect( previewButtonLocator ).toBeHidden();
} ); } );
} ); } );
test( 'Product picker should be shown when selected product is deleted', async ( {
pageObject,
admin,
editor,
requestUtils,
page,
} ) => {
// Add a new test product to the database
let testProductId: number | null = null;
const newProduct = await requestUtils.rest( {
method: 'POST',
path: 'wc/v3/products',
data: {
name: 'A Test Product',
price: 10,
},
} );
testProductId = newProduct.id;
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,
'A Test Product'
);
await expect( editorProductPicker ).toBeHidden();
await editor.saveDraft();
// Delete the product
await requestUtils.rest( {
method: 'DELETE',
path: `wc/v3/products/${ testProductId }`,
} );
// Product picker should be shown in Editor
await admin.page.reload();
const deletedProductPicker = editor.canvas.getByText(
'Previously selected product'
);
await expect( deletedProductPicker ).toBeVisible();
// Change status from "trash" to "publish"
await requestUtils.rest( {
method: 'PUT',
path: `wc/v3/products/${ testProductId }`,
data: {
status: 'publish',
},
} );
// Product Picker shouldn't be shown as product is available now
await page.reload();
await expect( editorProductPicker ).toBeHidden();
// Delete the product from database, instead of trashing it
await requestUtils.rest( {
method: 'DELETE',
path: `wc/v3/products/${ testProductId }`,
params: {
// Bypass trash and permanently delete the product
force: true,
},
} );
// Product picker should be shown in Editor
await expect( deletedProductPicker ).toBeVisible();
} );
} ); } );

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Product Collection: Added Editor UI for missing product reference

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Expand the e2e suite we're running on WPCOM part #2.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix error when adding the Product Gallery (Beta) block into a pattern

View File

@ -110,11 +110,14 @@ class ProductGallery extends AbstractBlock {
* @return string Rendered block type output. * @return string Rendered block type output.
*/ */
protected function render( $attributes, $content, $block ) { protected function render( $attributes, $content, $block ) {
$post_id = $block->context['postId'] ?? ''; $post_id = $block->context['postId'] ?? '';
$product = wc_get_product( $post_id );
if ( ! $product instanceof \WC_Product ) {
return '';
}
$product_gallery_images = ProductGalleryUtils::get_product_gallery_images( $post_id, 'thumbnail', array() ); $product_gallery_images = ProductGalleryUtils::get_product_gallery_images( $post_id, 'thumbnail', array() );
$classname_single_image = ''; $classname_single_image = '';
// This is a temporary solution. We have to refactor this code when the block will have to be addable on every page/post https://github.com/woocommerce/woocommerce-blocks/issues/10882.
global $product;
if ( count( $product_gallery_images ) < 2 ) { if ( count( $product_gallery_images ) < 2 ) {
// The gallery consists of a single image. // The gallery consists of a single image.
@ -124,8 +127,6 @@ class ProductGallery extends AbstractBlock {
$number_of_thumbnails = $block->attributes['thumbnailsNumberOfThumbnails'] ?? 0; $number_of_thumbnails = $block->attributes['thumbnailsNumberOfThumbnails'] ?? 0;
$classname = $attributes['className'] ?? ''; $classname = $attributes['className'] ?? '';
$dialog = isset( $attributes['mode'] ) && 'full' !== $attributes['mode'] ? $this->render_dialog() : ''; $dialog = isset( $attributes['mode'] ) && 'full' !== $attributes['mode'] ? $this->render_dialog() : '';
$post_id = $block->context['postId'] ?? '';
$product = wc_get_product( $post_id );
$product_gallery_first_image = ProductGalleryUtils::get_product_gallery_image_ids( $product, 1 ); $product_gallery_first_image = ProductGalleryUtils::get_product_gallery_image_ids( $product, 1 );
$product_gallery_first_image_id = reset( $product_gallery_first_image ); $product_gallery_first_image_id = reset( $product_gallery_first_image );
$product_id = strval( $product->get_id() ); $product_id = strval( $product->get_id() );

View File

@ -26,7 +26,7 @@ class ProductGalleryUtils {
$product_gallery_images = array(); $product_gallery_images = array();
$product = wc_get_product( $post_id ); $product = wc_get_product( $post_id );
if ( $product ) { if ( $product instanceof \WC_Product ) {
$all_product_gallery_image_ids = self::get_product_gallery_image_ids( $product ); $all_product_gallery_image_ids = self::get_product_gallery_image_ids( $product );
if ( 'full' === $size || 'full' !== $size && count( $all_product_gallery_image_ids ) > 1 ) { if ( 'full' === $size || 'full' !== $size && count( $all_product_gallery_image_ids ) > 1 ) {

View File

@ -15,6 +15,14 @@ config = {
'**/admin-tasks/**/*.spec.js', '**/admin-tasks/**/*.spec.js',
'**/shopper/**/*.spec.js', '**/shopper/**/*.spec.js',
'**/api-tests/**/*.test.js', '**/api-tests/**/*.test.js',
'**/merchant/products/add-variable-product/**/*.spec.js',
'**/merchant/command-palette.spec.js',
'**/merchant/create-cart-block.spec.js',
'**/merchant/create-checkout-block.spec.js',
'**/merchant/create-coupon.spec.js',
'**/merchant/create-order.spec.js',
'**/merchant/create-page.spec.js',
'**/merchant/create-post.spec.js',
], ],
grepInvert: /@skip-on-default-wpcom/, grepInvert: /@skip-on-default-wpcom/,
}, },

View File

@ -149,9 +149,11 @@ test.describe(
.locator( 'legend' ) .locator( 'legend' )
).toBeVisible(); ).toBeVisible();
await expect( await expect(
page.locator( page
'.wp-block-woocommerce-checkout-order-summary-block' .locator(
) '.wp-block-woocommerce-checkout-order-summary-block'
)
.first()
).toBeVisible(); ).toBeVisible();
await expect( await expect(
page.locator( '.wc-block-components-address-form' ).first() page.locator( '.wc-block-components-address-form' ).first()

View File

@ -39,102 +39,110 @@ const test = baseTest.extend( {
}, },
} ); } );
test.describe( 'Coupon management', { tag: '@services' }, () => { test.describe(
for ( const couponType of Object.keys( couponData ) ) { 'Coupon management',
test( `can create new ${ couponType } coupon`, async ( { { tag: [ '@services', '@skip-on-default-wpcom' ] },
page, () => {
coupon, for ( const couponType of Object.keys( couponData ) ) {
} ) => { test( `can create new ${ couponType } coupon`, async ( {
await test.step( 'add new coupon', async () => { page,
await page.goto( coupon,
'wp-admin/post-new.php?post_type=shop_coupon' } ) => {
); await test.step( 'add new coupon', async () => {
await page await page.goto(
.getByLabel( 'Coupon code' ) 'wp-admin/post-new.php?post_type=shop_coupon'
.fill( couponData[ couponType ].code ); );
await page await page
.getByPlaceholder( 'Description (optional)' ) .getByLabel( 'Coupon code' )
.fill( couponData[ couponType ].description ); .fill( couponData[ couponType ].code );
await page await page
.getByPlaceholder( '0' ) .getByPlaceholder( 'Description (optional)' )
.fill( couponData[ couponType ].amount ); .fill( couponData[ couponType ].description );
await page
.getByPlaceholder( '0' )
.fill( couponData[ couponType ].amount );
// set expiry date if it was provided // set expiry date if it was provided
if ( couponData[ couponType ].expiryDate ) {
await page
.getByPlaceholder( 'yyyy-mm-dd' )
.fill( couponData[ couponType ].expiryDate );
}
// be explicit about whether free shipping is allowed
if ( couponData[ couponType ].freeShipping ) {
await page.getByLabel( 'Allow free shipping' ).check();
} else {
await page
.getByLabel( 'Allow free shipping' )
.uncheck();
}
} );
// publish the coupon and retrieve the id
await test.step( 'publish the coupon', async () => {
await expect(
page.getByRole( 'link', { name: 'Move to Trash' } )
).toBeVisible();
await page
.getByRole( 'button', { name: 'Publish', exact: true } )
.click();
await expect(
page.getByText( 'Coupon updated.' )
).toBeVisible();
coupon.id = page.url().match( /(?<=post=)\d+/ )[ 0 ];
expect( coupon.id ).toBeDefined();
} );
// verify the creation of the coupon and details
await test.step( 'verify coupon creation', async () => {
await page.goto(
'wp-admin/edit.php?post_type=shop_coupon'
);
await expect(
page.getByRole( 'cell', {
name: couponData[ couponType ].code,
} )
).toBeVisible();
await expect(
page.getByRole( 'cell', {
name: couponData[ couponType ].description,
} )
).toBeVisible();
await expect(
page.getByRole( 'cell', {
name: couponData[ couponType ].amount,
exact: true,
} )
).toBeVisible();
} );
// check expiry date if it was set
if ( couponData[ couponType ].expiryDate ) { if ( couponData[ couponType ].expiryDate ) {
await page await test.step( 'verify coupon expiry date', async () => {
.getByPlaceholder( 'yyyy-mm-dd' ) await page
.fill( couponData[ couponType ].expiryDate ); .getByText( couponData[ couponType ].code )
.last()
.click();
await expect(
page.getByPlaceholder( 'yyyy-mm-dd' )
).toHaveValue( couponData[ couponType ].expiryDate );
} );
} }
// be explicit about whether free shipping is allowed // if it was a free shipping coupon check that
if ( couponData[ couponType ].freeShipping ) { if ( couponData[ couponType ].freeShipping ) {
await page.getByLabel( 'Allow free shipping' ).check(); await test.step( 'verify free shipping', async () => {
} else { await page
await page.getByLabel( 'Allow free shipping' ).uncheck(); .getByText( couponData[ couponType ].code )
.last()
.click();
await expect(
page.getByLabel( 'Allow free shipping' )
).toBeChecked();
} );
} }
} ); } );
}
// publish the coupon and retrieve the id
await test.step( 'publish the coupon', async () => {
await expect(
page.getByRole( 'link', { name: 'Move to Trash' } )
).toBeVisible();
await page
.getByRole( 'button', { name: 'Publish', exact: true } )
.click();
await expect(
page.getByText( 'Coupon updated.' )
).toBeVisible();
coupon.id = page.url().match( /(?<=post=)\d+/ )[ 0 ];
expect( coupon.id ).toBeDefined();
} );
// verify the creation of the coupon and details
await test.step( 'verify coupon creation', async () => {
await page.goto( 'wp-admin/edit.php?post_type=shop_coupon' );
await expect(
page.getByRole( 'cell', {
name: couponData[ couponType ].code,
} )
).toBeVisible();
await expect(
page.getByRole( 'cell', {
name: couponData[ couponType ].description,
} )
).toBeVisible();
await expect(
page.getByRole( 'cell', {
name: couponData[ couponType ].amount,
exact: true,
} )
).toBeVisible();
} );
// check expiry date if it was set
if ( couponData[ couponType ].expiryDate ) {
await test.step( 'verify coupon expiry date', async () => {
await page
.getByText( couponData[ couponType ].code )
.last()
.click();
await expect(
page.getByPlaceholder( 'yyyy-mm-dd' )
).toHaveValue( couponData[ couponType ].expiryDate );
} );
}
// if it was a free shipping coupon check that
if ( couponData[ couponType ].freeShipping ) {
await test.step( 'verify free shipping', async () => {
await page
.getByText( couponData[ couponType ].code )
.last()
.click();
await expect(
page.getByLabel( 'Allow free shipping' )
).toBeChecked();
} );
}
} );
} }
} ); );

View File

@ -84,105 +84,117 @@ test.describe( 'Add variations', { tag: '@gutenberg' }, () => {
} }
} ); } );
test( 'can manually add a variation', async ( { page } ) => { test(
await test.step( `Open "Edit product" page of product id ${ productId_addManually }`, async () => { 'can manually add a variation',
await page.goto( { tag: '@skip-on-default-wpcom' },
`/wp-admin/post.php?post=${ productId_addManually }&action=edit` async ( { page } ) => {
); await test.step( `Open "Edit product" page of product id ${ productId_addManually }`, async () => {
} ); await page.goto(
`/wp-admin/post.php?post=${ productId_addManually }&action=edit`
// hook up the woocommerce_variations_added jQuery trigger so we can check if it's fired );
await test.step( 'Hook up the woocommerce_variations_added jQuery trigger', async () => {
await page.evaluate( () => {
window.woocommerceVariationsAddedFunctionCalls = [];
window
.jQuery( '#variable_product_options' )
.on( 'woocommerce_variations_added', ( event, data ) => {
window.woocommerceVariationsAddedFunctionCalls.push( [
event,
data,
] );
} );
} ); } );
} );
await test.step( 'Click on the "Variations" tab.', async () => { // hook up the woocommerce_variations_added jQuery trigger so we can check if it's fired
await page.locator( '.variations_tab' ).click(); await test.step( 'Hook up the woocommerce_variations_added jQuery trigger', async () => {
} ); await page.evaluate( () => {
window.woocommerceVariationsAddedFunctionCalls = [];
await test.step( `Manually add ${ variationsToManuallyCreate.length } variations`, async () => { window
const variationRows = page.locator( '.woocommerce_variation h3' ); .jQuery( '#variable_product_options' )
let variationRowsCount = await variationRows.count(); .on(
const originalVariationRowsCount = variationRowsCount; 'woocommerce_variations_added',
( event, data ) => {
for ( const variationToCreate of variationsToManuallyCreate ) { window.woocommerceVariationsAddedFunctionCalls.push(
await test.step( 'Click "Add manually"', async () => { [ event, data ]
const addManuallyButton = page.getByRole( 'button', { );
name: 'Add manually', }
} );
await addManuallyButton.click();
await expect( variationRows ).toHaveCount(
++variationRowsCount
);
// verify that the woocommerce_variations_added jQuery trigger was fired
const woocommerceVariationsAddedFunctionCalls =
await page.evaluate(
() => window.woocommerceVariationsAddedFunctionCalls
); );
expect(
woocommerceVariationsAddedFunctionCalls.length
).toEqual(
variationRowsCount - originalVariationRowsCount
);
} ); } );
} );
for ( const attributeValue of variationToCreate ) { await test.step( 'Click on the "Variations" tab.', async () => {
const attributeName = productAttributes.find( await page.locator( '.variations_tab' ).click();
( { options } ) => options.includes( attributeValue ) } );
).name;
const addAttributeMenu = variationRows await test.step( `Manually add ${ variationsToManuallyCreate.length } variations`, async () => {
.nth( 0 ) const variationRows = page.locator(
.locator( 'select', { '.woocommerce_variation h3'
has: page.locator( 'option', { );
hasText: attributeValue, let variationRowsCount = await variationRows.count();
} ), const originalVariationRowsCount = variationRowsCount;
for ( const variationToCreate of variationsToManuallyCreate ) {
await test.step( 'Click "Add manually"', async () => {
const addManuallyButton = page.getByRole( 'button', {
name: 'Add manually',
} ); } );
await test.step( `Select "${ attributeValue }" from the "${ attributeName }" attribute menu`, async () => { await addManuallyButton.click();
await addAttributeMenu.selectOption( attributeValue );
await expect( variationRows ).toHaveCount(
++variationRowsCount
);
// verify that the woocommerce_variations_added jQuery trigger was fired
const woocommerceVariationsAddedFunctionCalls =
await page.evaluate(
() =>
window.woocommerceVariationsAddedFunctionCalls
);
expect(
woocommerceVariationsAddedFunctionCalls.length
).toEqual(
variationRowsCount - originalVariationRowsCount
);
} ); } );
}
await test.step( 'Click "Save changes"', async () => {
await page
.getByRole( 'button', {
name: 'Save changes',
} )
.click();
} );
await test.step( `Expect the variation ${ variationToCreate.join(
', '
) } to be successfully saved.`, async () => {
let newlyAddedVariationRow;
for ( const attributeValue of variationToCreate ) { for ( const attributeValue of variationToCreate ) {
newlyAddedVariationRow = ( const attributeName = productAttributes.find(
newlyAddedVariationRow || variationRows ( { options } ) =>
).filter( { options.includes( attributeValue )
has: page.locator( 'option[selected]', { ).name;
hasText: attributeValue, const addAttributeMenu = variationRows
} ), .nth( 0 )
.locator( 'select', {
has: page.locator( 'option', {
hasText: attributeValue,
} ),
} );
await test.step( `Select "${ attributeValue }" from the "${ attributeName }" attribute menu`, async () => {
await addAttributeMenu.selectOption(
attributeValue
);
} ); } );
} }
await expect( newlyAddedVariationRow ).toBeVisible(); await test.step( 'Click "Save changes"', async () => {
} ); await page
} .getByRole( 'button', {
} ); name: 'Save changes',
} ); } )
.click();
} );
await test.step( `Expect the variation ${ variationToCreate.join(
', '
) } to be successfully saved.`, async () => {
let newlyAddedVariationRow;
for ( const attributeValue of variationToCreate ) {
newlyAddedVariationRow = (
newlyAddedVariationRow || variationRows
).filter( {
has: page.locator( 'option[selected]', {
hasText: attributeValue,
} ),
} );
}
await expect( newlyAddedVariationRow ).toBeVisible();
} );
}
} );
}
);
} ); } );