Product Collection - Add Editor UI for missing product reference (#51114)
* Initial implementation of the missing product state - Changed `getProductCollectionUIStateInEditor` to a hook `useProductCollectionUIState`. - As we added logic to check if selected product reference is deleted which require making an API call. Therefore, I decided to use a hook. - While making an API call to check if product reference is deleted, I decided to show spinner in the block. - Introduced new UI state `DELETED_PRODUCT_REFERENCE` to handle the missing product state. - Updated existing `ProductPicker` component to handle the new UI state. - It's better to use existing component for the missing product state, as it already has all the required UI. * Add changefile(s) from automation for the following project(s): woocommerce-blocks * Use getEntityRecord to check if product exists and other improvements * Remove console log * Add E2E tests for deleted product reference in Product Collection block This commit introduces new E2E tests to verify the behavior of the Product Collection block when dealing with deleted product references. The changes include: 1. New test suite in register-product-collection-tester.block_theme.spec.ts 2. Modification to product-collection.page.ts to support custom product selection 3. Minor update to utils.tsx to handle trashed products These tests ensure that the Product Collection block correctly handles scenarios where referenced products are deleted, trashed, or restored, improving the overall reliability of the feature. * Simplify product creation in Product Collection block test * Refactor E2E test for delete product reference picker 1. Removing the unnecessary `test.describe` block for "Deleted product reference" 2. Eliminating the `beforeEach` hook that was creating a test product 3. Integrating the test product creation directly into the main test This change simplifies the test structure and improves readability while maintaining the same test coverage for the Product Collection block's behavior when dealing with deleted or unavailable products. * Simplify logic for product collection UI state This commit simplifies the handling of the `usesReference` prop in the Product Collection block: 1. In `edit/index.tsx`, directly pass `usesReference` to `useProductCollectionUIState` hook without conditional spreading. 2. In `utils.tsx`, update the type definition of `usesReference` in the `useProductCollectionUIState` hook to explicitly include `undefined`. * Refactor Product Collection block to improve prop passing - Introduce ProductCollectionContentProps type for better prop management - Refactor Edit component to use a renderComponent function - Update component prop types to use more specific props - Remove unnecessary props from ProductCollectionEditComponentProps - Simplify component rendering logic in Edit component - Adjust ProductPicker to receive only required props - Update imports and prop types in various files to use new types This refactoring improves code organization and reduces prop drilling by only passing necessary props to each component. It enhances maintainability and readability of the Product Collection block and related components. * Refactor Product Collection block editor UI state handling This commit simplifies the rendering logic in the Product Collection block's Edit component. It removes a redundant case for the VALID state, as both VALID and VALID_WITH_PREVIEW states now render the same ProductCollectionContent component. This change improves code maintainability and reduces duplication. * Fix: isDeletedProductReference is not set correctly * Use "page.reload" instead of "admin.page.reload" * Separate PRODUCT_REFERENCE_PICKER and DELETED_PRODUCT_REFERENCE cases This improves readability and maintainability of the switch statement. --------- Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
parent
4ee2689f4f
commit
861bc091d4
|
@ -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={
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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 = ( {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Product Collection: Added Editor UI for missing product reference
|
Loading…
Reference in New Issue