Product Collection: Implement Preview Mode (#46369)

* POC: Preview Mode using HOC

* Add explanation as comments

POC: Implement preview mode for Product Collection block in editor
- Added extensive commentary to clarify the mechanism and usage of the `handlePreviewState` function
- Implemented an internal state within `ProductCollectionContent` to manage preview status and messages, serving as a foundational example of how preview mode can enrich block functionality.
- Showcased the application of `handlePreviewState` by incorporating it as a prop in `BlockEdit`, illustrating the potential for extending the block's capabilities for dynamic and interactive previews.

This POC demonstrates a flexible approach to managing preview states within the editor, paving the way for further development and integration based on feedback and use-case analysis.

* Refactor preview state handling and collection registration

This commit introduces a centralized approach for registering product collection variations and managing their preview states. It abstracts the registration logic into a dedicated function and enhances the flexibility of preview state handling across different collection types.

* Rename file

* Minor improvements

* Don't pass previewState to handlePreviewState

I don't see any good use of it in handlePreviewState. Also, We will be going to call handlePreviewState only once
therefore, it will always have the same value as the initial value of the previewState. If in future, we decide to run it
multiple times then we can pass the previewState as an argument to handlePreviewState.

* Add comment

* Use JS closure to inject handlePreviewState

This commit introduces a refined approach for injecting the `handlePreviewState` function into product collection blocks, utilizing JavaScript closures to streamline the process. This method replaces the previous global registry mechanism, offering a more direct and efficient way to manage preview states.

Advantages of This Approach:
- Utilizing JavaScript closures for injecting `handlePreviewState` simplifies the overall architecture by directly modifying block edit components without relying on an external registry. This method enhances code clarity and reduces the cognitive load for developers.
- The conditional application of `withHandlePreviewState` ensures that the preview state handling logic is only added to blocks that require it, optimizing performance and maintainability.

* Refactor preview state management into custom hook

This commit enhances the organization and readability of the product collection content component by abstracting the preview state management into a custom hook named `usePreviewState`. This change streamlines the component's structure and aligns with React best practices for managing state and side effects.

Key Changes:
- Introduced `usePreviewState`, a custom hook responsible for initializing and managing the preview state (`isPreview` and `previewMessage`) of the product collection block. This hook encapsulates the state logic and its side effects, including the conditional invocation of `handlePreviewState`.
- Modified `ProductCollectionContent` to utilize `usePreviewState` for handling its preview state. This adjustment makes the component cleaner and focuses it more on presentation and behavior rather than state management details.

* Replace useEffect with useLayoutEffect

* Add cleanup function in handlePreviewState function

Based on [this discussion](https://github.com/woocommerce/woocommerce/pull/45703#discussion_r1535323883), I added a cleanup function support for handlePreviewState. `handlePreviewState` can return a function which will be called on cleanup in `useLayoutEffect` hook.

* Fetching random products in Preview mode

* Allow collection to set initial preview state

* Pass location & all attributes to handlePreviewState function

* Handling collection specific query for preview mode

- Consolidated `handlePreviewState` and `initialPreviewState` into a single `preview` prop in `register-product-collection.tsx` and `product-collection-content.tsx` to streamline prop passing and improve the component interface.
- Updated the `queryContextIncludes` in `constants.ts` to include 'previewState'
- Enhanced the `ProductCollection` PHP class to handle preview-specific queries more effectively, introducing a new method `get_preview_query_args` that adjusts query parameters based on the collection being previewed, thereby improving the relevance and accuracy of products displayed in preview mode.

* Always set initialPreviewState on load

* Refine preview state handling

- Renamed `HandlePreviewStateArgs` to `SetPreviewStateArgs` in `featured.tsx` to better reflect its purpose, which is now more focused on setting rather than handling states. The implementation details within `featured.tsx` have also been refined to include async operations and cleanup functions, demonstrating a more sophisticated approach to managing state.

Overall, these updates make the preview state logic more understandable and maintainable.

* Rename "initialState" to "initialPreviewState"

* Fix: Correct merging of newPreviewState into previewState attribute

This commit addresses an issue in the product-collection-content.tsx where the newPreviewState was not properly merged into the existing previewState attribute. Previously, the spread operator was incorrectly applied, leading to potential loss of existing state attributes. By changing the order of operations and correctly spreading the existing attributes before merging the newPreviewState, we ensure that all state attributes are preserved and updated correctly.

* Initial refactor POC code to productionize it

* Move `useSetPreviewState` to Utils

* Implement preview mode for Generic archive templates

Implemented a new useLayoutEffect in `utils.tsx` to dynamically set a preview message in the editor for product collection blocks located in generic archive templates (like Products by Category, Products by Tag, or Products by Attribute).

* Remove preview mode from Featured and On sale collection

* Remove preview query modfication for On Sale collection

* Add changefile(s) from automation for the following project(s): woocommerce-blocks, woocommerce

* Fix: hide/show preview label based on value of "inherit"

If user change the toggle "Sync with current query", then it should reflect for the preview label as well.
- If the toggle is on, then the preview label should be shown.
- If the toggle is off, then the preview label should be hidden.

* Minor improvements

* Add changefile(s) from automation for the following project(s): woocommerce-blocks, woocommerce

* Add changefile(s) from automation for the following project(s): woocommerce-blocks, woocommerce

* Refactor: Simplify SetPreviewState type definition in types.ts

This commit refines the SetPreviewState type definition by eliminating the previously used intermediate interface, SetPreviewStateArgs. The change streamlines the type definition directly within the SetPreviewState type, enhancing readability and reducing redundancy.

* Update import syntax for ElementType in register-product-collection.tsx

This commit updates the import statement for `ElementType` from `@wordpress/element` to use the more modern and concise `import type` syntax. This change does not affect functionality but aligns with TypeScript best practices for importing types, ensuring that type imports are distinguished from regular imports. This helps in clarity and in optimizing the build process by explicitly indicating that `ElementType` is used solely for type checking and not included in the JavaScript runtime.

* Refactor: Update TypeScript usage in Product Collection

This commit introduces several TypeScript refinements across product-collection components:

1. **DEFAULT_ATTRIBUTES** in `constants.ts` now uses `Pick` to explicitly define its shape, ensuring only relevant attributes are included and typed accurately.
2. **ProductCollectionAdvancedInspectorControls** and **ToolbarControls** in the `edit` subdirectory now use `Omit` to exclude the 'preview' property from props, clarifying the intended prop usage and improving type safety.

These changes collectively tighten the type definitions and improve the codebase's adherence to best practices in TypeScript.

* Refactor: Update dependencies of useSetPreviewState hook in utils.tsx

This change enhances the stability and predictability of the hook's behavior, ensuring it updates its internal state accurately when its dependencies change, thus aligning with best practices in React development.

* Refactor preview button CSS and conditional rendering

1. **CSS Refactoring:** Moved the positioning styles of the `.wc-block-product-collection__preview-button` from inline styles in the JSX to the `editor.scss` file. This separation of concerns improves maintainability and readability, aligning the styling responsibilities solely within the CSS file.
2. **Conditional Rendering Logic:** Updated the rendering condition for the preview button. Now, it not only checks if `isPreview` is true but also if the block is currently selected (`props.isSelected`). This prevents the preview button from appearing when the block is not actively selected, reducing visual clutter and enhancing the user experience in the editor.

* Enhance: Update preview button visibility logic in ProductCollectionContent

This commit enhances the visibility logic of the preview button in the `ProductCollectionContent` component:

1. **Added `isSelectedOrInnerBlockSelected`:** Introduced a new `useSelect` hook to determine if the current block or any of its inner blocks are selected. This ensures that the preview button is visible when either the product collection block or any of its inner blocks are selected.
2. **Updated Conditional Rendering:** Adjusted the conditional rendering of the preview button to use the new `isSelectedOrInnerBlockSelected` value, providing a more intuitive user experience by ensuring the preview button remains visible when any relevant block is selected.

* use __private prefix with attribute name

* Add E2E tests for Preview Mode

1. **Template-Specific Tests:** Each template (tag, category, attribute) undergoes a test to ensure the preview button behaves as expected when replacing products with product collections in these contexts.
2. **Visibility Checks:** The tests verify that the preview button is visible when the block or its inner blocks are selected and hidden when the block is not selected. This helps confirm the correct implementation of the preview button visibility logic across different use cases.
3. **Interaction with Inner Blocks:** Additional checks are included to ensure the preview button's visibility toggles appropriately when interacting with inner blocks, reinforcing the dynamic nature of block selection and its effect on UI elements within the editor.

* Add setPreviewState to dependencies

* Add data-test-id to Preview button and update e2e locator

Modifications:
- Added `data-test-id="product-collection-preview-button"` to the Preview button in `product-collection-content.tsx`.
- Updated the corresponding e2e test locator in `product-collection.block_theme.side_effects.spec.ts` to use the new `data-test-id` instead of the class name.

By using `data-test-id`, we ensure that the e2e tests are not affected by changes in the styling or restructuring of the DOM that might alter CSS classes but do not affect functionality.

* Enhance: Localize preview message in useSetPreviewState hook

* Don't show shadow & outline on focus

* Make preview button font same as Admin

* Fix SCSS lint errors

* Add missing await keyword

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Manish Menaria 2024-05-15 13:18:43 +05:30 committed by GitHub
parent e50c8426fb
commit 9f5f93cba6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 380 additions and 22 deletions

View File

@ -38,6 +38,9 @@
"forcePageReload": {
"type": "boolean",
"default": false
},
"__privatePreviewState": {
"type": "object"
}
},
"providesContext": {
@ -45,7 +48,8 @@
"query": "query",
"displayLayout": "displayLayout",
"queryContextIncludes": "queryContextIncludes",
"collection": "collection"
"collection": "collection",
"__privateProductCollectionPreviewState": "__privatePreviewState"
},
"usesContext": [ "templateSlug" ],
"supports": {

View File

@ -6,7 +6,6 @@ import {
// @ts-expect-error Type definition is missing
store as blocksStore,
type BlockVariation,
registerBlockVariation,
BlockAttributes,
} from '@wordpress/blocks';
@ -21,6 +20,7 @@ import topRated from './top-rated';
import bestSellers from './best-sellers';
import onSale from './on-sale';
import featured from './featured';
import registerProductCollection from './register-product-collection';
const collections: BlockVariation[] = [
productCollection,
@ -40,7 +40,7 @@ export const registerCollections = () => {
return blockAttrs.collection === variationAttributes.collection;
};
registerBlockVariation( blockJson.name, {
registerProductCollection( {
isActive,
...collection,
} );

View File

@ -0,0 +1,72 @@
/**
* External dependencies
*/
import { BlockVariation, registerBlockVariation } from '@wordpress/blocks';
import { addFilter } from '@wordpress/hooks';
import { EditorBlock } from '@woocommerce/types';
import type { ElementType } from '@wordpress/element';
import type { BlockEditProps } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import blockJson from '../block.json';
import {
SetPreviewState,
PreviewState,
ProductCollectionAttributes,
} from '../types';
export interface ProductCollectionConfig extends BlockVariation {
preview?: {
setPreviewState?: SetPreviewState;
initialPreviewState?: PreviewState;
};
}
/**
* Register a new collection for the Product Collection block.
*
* @param {ProductCollectionConfig} blockVariationArgs The configuration of new collection.
*/
const registerProductCollection = ( {
preview: { setPreviewState, initialPreviewState } = {},
...blockVariationArgs
}: ProductCollectionConfig ) => {
/**
* If setPreviewState or initialPreviewState is provided, inject the setPreviewState & initialPreviewState props.
* This is useful for handling preview mode in the editor.
*/
if ( setPreviewState || initialPreviewState ) {
const withSetPreviewState =
< T extends EditorBlock< T > >( BlockEdit: ElementType ) =>
( props: BlockEditProps< ProductCollectionAttributes > ) => {
// If collection name does not match, return the original BlockEdit component.
if ( props.attributes.collection !== blockVariationArgs.name ) {
return <BlockEdit { ...props } />;
}
// Otherwise, inject the setPreviewState & initialPreviewState props.
return (
<BlockEdit
{ ...props }
preview={ {
setPreviewState,
initialPreviewState,
} }
/>
);
};
addFilter(
'editor.BlockEdit',
blockVariationArgs.name,
withSetPreviewState
);
}
registerBlockVariation( blockJson.name, {
...blockVariationArgs,
} );
};
export default registerProductCollection;

View File

@ -63,7 +63,14 @@ export const DEFAULT_QUERY: ProductCollectionQuery = {
priceRange: undefined,
};
export const DEFAULT_ATTRIBUTES: Partial< ProductCollectionAttributes > = {
export const DEFAULT_ATTRIBUTES: Pick<
ProductCollectionAttributes,
| 'query'
| 'tagName'
| 'displayLayout'
| 'queryContextIncludes'
| 'forcePageReload'
> = {
query: DEFAULT_QUERY,
tagName: 'div',
displayLayout: {

View File

@ -1,8 +1,8 @@
$max-columns: 3;
$min-button-width: 250px;
$gap-count: calc(#{ $max-columns } - 1);
$total-gap-width: calc(#{ $gap-count } * #{ $gap-small });
$max-button-width: calc((100% - #{ $total-gap-width }) / #{ $max-columns });
$gap-count: calc(#{$max-columns} - 1);
$total-gap-width: calc(#{$gap-count} * #{$gap-small});
$max-button-width: calc((100% - #{$total-gap-width}) / #{$max-columns});
.wc-block-editor-product-collection-inspector-toolspanel__filters {
.wc-block-editor-product-collection-inspector__taxonomy-control:not(:last-child) {
@ -35,7 +35,7 @@ $max-button-width: calc((100% - #{ $total-gap-width }) / #{ $max-columns });
.wc-blocks-product-collection__collections-section {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(max(#{ $min-button-width }, #{ $max-button-width }), 1fr));
grid-template-columns: repeat(auto-fill, minmax(max(#{$min-button-width}, #{$max-button-width}), 1fr));
grid-auto-rows: 1fr;
grid-gap: $gap-small;
margin: $gap-large auto;
@ -102,3 +102,17 @@ $max-button-width: calc((100% - #{ $total-gap-width }) / #{ $max-columns });
pointer-events: none;
}
}
// Preview mode
.wc-block-product-collection__preview-button {
position: absolute;
top: 0;
right: 0;
transform: translateY(-100%);
font-family: var(--wp--preset--font-family--system-sans-serif);
&.is-primary:focus:not(:disabled) {
outline: none;
box-shadow: none;
}
}

View File

@ -2,20 +2,19 @@
* External dependencies
*/
import { store as blockEditorStore } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
import { useState } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import type { ProductCollectionAttributes } from '../types';
import type { ProductCollectionEditComponentProps } from '../types';
import ProductCollectionPlaceholder from './product-collection-placeholder';
import ProductCollectionContent from './product-collection-content';
import CollectionSelectionModal from './collection-selection-modal';
import './editor.scss';
const Edit = ( props: BlockEditProps< ProductCollectionAttributes > ) => {
const Edit = ( props: ProductCollectionEditComponentProps ) => {
const { clientId, attributes } = props;
const [ isSelectionModalOpen, setIsSelectionModalOpen ] = useState( false );

View File

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

View File

@ -7,8 +7,10 @@ import {
store as blockEditorStore,
} from '@wordpress/block-editor';
import { useInstanceId } from '@wordpress/compose';
import { useEffect } from '@wordpress/element';
import { useEffect, useRef } from '@wordpress/element';
import { Button } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { useGetLocation } from '@woocommerce/blocks/product-template/utils';
/**
* Internal dependencies
@ -19,15 +21,28 @@ import type {
ProductCollectionEditComponentProps,
} from '../types';
import { DEFAULT_ATTRIBUTES, INNER_BLOCKS_TEMPLATE } from '../constants';
import { getDefaultValueOfInheritQueryFromTemplate } from '../utils';
import {
getDefaultValueOfInheritQueryFromTemplate,
useSetPreviewState,
} from '../utils';
import InspectorControls from './inspector-controls';
import InspectorAdvancedControls from './inspector-advanced-controls';
import ToolbarControls from './toolbar-controls';
const ProductCollectionContent = (
props: ProductCollectionEditComponentProps
) => {
const ProductCollectionContent = ( {
preview: { setPreviewState, initialPreviewState } = {},
...props
}: ProductCollectionEditComponentProps ) => {
const isInitialAttributesSet = useRef( false );
const { clientId, attributes, setAttributes } = props;
const location = useGetLocation( props.context, props.clientId );
useSetPreviewState( {
setPreviewState,
setAttributes,
location,
attributes,
} );
const blockProps = useBlockProps();
const innerBlocksProps = useInnerBlocksProps( blockProps, {
@ -75,13 +90,38 @@ const ProductCollectionContent = (
},
...( attributes as Partial< ProductCollectionAttributes > ),
queryId,
// If initialPreviewState is provided, set it as previewState.
...( !! attributes.collection && {
__privatePreviewState: initialPreviewState,
} ),
} );
isInitialAttributesSet.current = true;
},
// This hook is only needed on initialization and sets default attributes.
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const isSelectedOrInnerBlockSelected = useSelect(
( select ) => {
const { getSelectedBlockClientId, hasSelectedInnerBlock } =
select( 'core/block-editor' );
// Check if the current block is selected.
const isSelected = getSelectedBlockClientId() === clientId;
// Check if any inner block of the current block is selected.
const isInnerBlockSelected = hasSelectedInnerBlock(
clientId,
true
);
return isSelected || isInnerBlockSelected;
},
[ clientId ]
);
/**
* If inherit is not a boolean, then we haven't set default attributes yet.
* We don't wanna render anything until default attributes are set.
@ -91,8 +131,29 @@ const ProductCollectionContent = (
return null;
}
// Let's not render anything until default attributes are set.
if ( ! isInitialAttributesSet.current ) {
return null;
}
return (
<div { ...blockProps }>
{ attributes.__privatePreviewState?.isPreview &&
isSelectedOrInnerBlockSelected && (
<Button
variant="primary"
size="small"
showTooltip
label={
attributes.__privatePreviewState?.previewMessage
}
className="wc-block-product-collection__preview-button"
data-test-id="product-collection-preview-button"
>
Preview
</Button>
) }
<InspectorControls { ...props } />
<InspectorAdvancedControls { ...props } />
<ToolbarControls { ...props } />

View File

@ -14,7 +14,7 @@ import CollectionChooserToolbar from './collection-chooser-toolbar';
import type { ProductCollectionEditComponentProps } from '../../types';
export default function ToolbarControls(
props: ProductCollectionEditComponentProps
props: Omit< ProductCollectionEditComponentProps, 'preview' >
) {
const { attributes, openCollectionSelectionModal, setAttributes } = props;
const { query, displayLayout } = attributes;

View File

@ -4,6 +4,11 @@
import type { BlockEditProps } from '@wordpress/blocks';
import { type AttributeMetadata } from '@woocommerce/types';
/**
* Internal dependencies
*/
import { WooCommerceBlockLocation } from '../product-template/utils';
export interface ProductCollectionAttributes {
query: ProductCollectionQuery;
queryId: number;
@ -23,6 +28,8 @@ export interface ProductCollectionAttributes {
*/
queryContextIncludes: string[];
forcePageReload: boolean;
// eslint-disable-next-line @typescript-eslint/naming-convention
__privatePreviewState?: PreviewState;
}
export enum LayoutOptions {
@ -91,6 +98,10 @@ export interface ProductCollectionQuery {
export type ProductCollectionEditComponentProps =
BlockEditProps< ProductCollectionAttributes > & {
openCollectionSelectionModal: () => void;
preview: {
initialPreviewState?: PreviewState;
setPreviewState?: SetPreviewState;
};
context: {
templateSlug: string;
};
@ -143,3 +154,14 @@ export enum CoreFilterNames {
export type CollectionName = CoreCollectionNames | string;
export type FilterName = CoreFilterNames | string;
export interface PreviewState {
isPreview: boolean;
previewMessage: string;
}
export type SetPreviewState = ( args: {
setState: ( previewState: PreviewState ) => void;
location: WooCommerceBlockLocation;
attributes: ProductCollectionAttributes;
} ) => void | ( () => void );

View File

@ -6,13 +6,24 @@ 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 { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { ProductCollectionAttributes, ProductCollectionQuery } from './types';
import {
PreviewState,
ProductCollectionAttributes,
ProductCollectionQuery,
SetPreviewState,
} from './types';
import { coreQueryPaginationBlockName } from './constants';
import blockJson from './block.json';
import {
LocationType,
WooCommerceBlockLocation,
} from '../product-template/utils';
/**
* Sets the new query arguments of a Product Query block
@ -109,3 +120,79 @@ export const addProductCollectionBlockToParentOfPaginationBlock = () => {
);
}
};
export const useSetPreviewState = ( {
setPreviewState,
location,
attributes,
setAttributes,
}: {
setPreviewState?: SetPreviewState | undefined;
location: WooCommerceBlockLocation;
attributes: ProductCollectionAttributes;
setAttributes: (
attributes: Partial< ProductCollectionAttributes >
) => void;
} ) => {
const setState = ( newPreviewState: PreviewState ) => {
setAttributes( {
__privatePreviewState: {
...attributes.__privatePreviewState,
...newPreviewState,
},
} );
};
// Running setPreviewState function provided by Collection, if it exists.
useLayoutEffect( () => {
if ( ! setPreviewState ) {
return;
}
const cleanup = setPreviewState?.( {
setState,
location,
attributes,
} );
if ( cleanup ) {
return cleanup;
}
// It should re-run only when setPreviewState changes to avoid performance issues.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ setPreviewState ] );
/**
* For all Product Collection blocks that inherit query from the template,
* we want to show a preview message in the editor if the block is in
* generic archive template i.e.
* - Products by category
* - Products by tag
* - Products by attribute
*/
useLayoutEffect( () => {
if ( ! setPreviewState ) {
const isGenericArchiveTemplate =
location.type === LocationType.Archive &&
location.sourceData?.termId === null;
if ( isGenericArchiveTemplate ) {
setAttributes( {
__privatePreviewState: {
isPreview: !! attributes?.query?.inherit,
previewMessage: __(
'Actual products will vary depending on the page being viewed.',
'woocommerce'
),
},
} );
}
}
}, [
attributes?.query?.inherit,
location.sourceData?.termId,
location.type,
setAttributes,
setPreviewState,
] );
};

View File

@ -15,7 +15,8 @@
"templateSlug",
"postId",
"queryContextIncludes",
"collection"
"collection",
"__privateProductCollectionPreviewState"
],
"supports": {
"inserter": false,

View File

@ -156,6 +156,7 @@ const ProductTemplateEdit = (
shrinkColumns: false,
},
queryContextIncludes = [],
__privateProductCollectionPreviewState,
},
__unstableLayoutClassNames,
} = props;
@ -249,6 +250,7 @@ const ProductTemplateEdit = (
...restQueryArgs,
location,
productCollectionQueryContext,
previewState: __privateProductCollectionPreviewState,
} ),
blocks: getBlocks( clientId ),
};
@ -270,6 +272,7 @@ const ProductTemplateEdit = (
location,
productCollectionQueryContext,
loopShopPerPage,
__privateProductCollectionPreviewState,
]
);
const blockContexts = useMemo(

View File

@ -63,7 +63,14 @@ const prepareIsInGenericTemplate =
( entitySlug: string ): boolean =>
templateSlug === entitySlug;
const createLocationObject = ( type: LocationType, sourceData = {} ) => ( {
export type WooCommerceBlockLocation = ReturnType<
typeof createLocationObject
>;
const createLocationObject = (
type: LocationType,
sourceData: Record< string, unknown > = {}
) => ( {
type,
sourceData,
} );

View File

@ -1082,4 +1082,51 @@ test.describe( 'Product Collection', () => {
);
} );
} );
test.describe( 'Preview mode in generic archive templates', () => {
const genericArchiveTemplates = [
{
name: 'Products by Tag',
path: 'woocommerce/woocommerce//taxonomy-product_tag',
},
{
name: 'Products by Category',
path: 'woocommerce/woocommerce//taxonomy-product_cat',
},
{
name: 'Products by Attribute',
path: 'woocommerce/woocommerce//taxonomy-product_attribute',
},
];
genericArchiveTemplates.forEach( ( { name, path } ) => {
test( `${ name } template`, async ( { page, pageObject } ) => {
await pageObject.replaceProductsWithProductCollectionInTemplate(
path
);
const editorFrame = page.frameLocator(
'iframe[name="editor-canvas"]'
);
const previewButtonLocator = editorFrame.locator(
'button[data-test-id="product-collection-preview-button"]'
);
// The preview button should be visible
await expect( previewButtonLocator ).toBeVisible();
// The preview button should be hidden when the block is not selected
await page.click( 'body' );
await expect( previewButtonLocator ).toBeHidden();
// Preview button should be visible when any of inner block is selected
await editorFrame
.getByLabel( 'Block: Product Template' )
.getByLabel( 'Block: Product Image' )
.first()
.click();
await expect( previewButtonLocator ).toBeVisible();
} );
} );
} );
} );

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Product Collection: Add preview mode on Editor side

View File

@ -84,7 +84,6 @@ class ProductCollection extends AbstractBlock {
// Disable client-side-navigation if incompatible blocks are detected.
add_filter( 'render_block_data', array( $this, 'disable_enhanced_pagination' ), 10, 1 );
}
/**
@ -370,6 +369,14 @@ class ProductCollection extends AbstractBlock {
return $args;
}
// Is this a preview mode request?
// If yes, short-circuit the query and return the preview query args.
$product_collection_query_context = $request->get_param( 'productCollectionQueryContext' );
$is_preview = $product_collection_query_context['previewState']['isPreview'] ?? false;
if ( 'true' === $is_preview ) {
return $this->get_preview_query_args( $args, $request );
}
$orderby = $request->get_param( 'orderBy' );
$on_sale = $request->get_param( 'woocommerceOnSale' ) === 'true';
$stock_status = $request->get_param( 'woocommerceStockStatus' );
@ -531,6 +538,29 @@ class ProductCollection extends AbstractBlock {
return $result;
}
/**
* Get query args for preview mode. These query args will be used with WP_Query to fetch the products.
*
* @param array $args Query args.
* @param WP_REST_Request $request Request.
*/
private function get_preview_query_args( $args, $request ) {
$collection_query = array();
/**
* In future, Here we will modify the preview query based on the collection name. For example:
*
* $product_collection_query_context = $request->get_param( 'productCollectionQueryContext' );
* $collection_name = $product_collection_query_context['collection'] ?? '';
* if ( 'woocommerce/product-collection/on-sale' === $collection_name ) {
* $collection_query = $this->get_on_sale_products_query( true );
* }.
*/
$args = $this->merge_queries( $args, $collection_query );
return $args;
}
/**
* Extends allowed `collection_params` for the REST API
*