Product Collection: add control to toggle whether block is filterable in non archive context (#49627)

* Add syncWithFilters attribute to Product Collection

* Add SycnWithFilters Inspector Control to PC

* Minor import adjustments

* Move the controls to Inherit as they share the same labels

* Rename the attirbute to filterable

* Hide control for collections

* Consume the filterable

* Consume filterable attribute so it enables the filtering

* Bring back file format

* Bring back filterable

* Move filterable from attribute to query

* Improve checks

* Fix incorrent function name

* Add changelog

* Set the default filterable value on insert

* Update test selector

* Add E2E tests to #49627 (#49715)

* Remove unnecessary call to create new post

* Fix "Use page context" control tests

* Fix post ↔ template collection sync test

* Fix non-thenable linter errors

* Extend the "Use page context" default setting test

* Add test for filtering in non-archive context

* Add test for combining editor and front-end filters

* Fix lint

* Update plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/inherit-query-control.tsx

Co-authored-by: Manish Menaria <the.manish.menaria@gmail.com>

* Improve query properties access

* Rename inherit-query-control to use-page-context-control to better depict its purpose

* Lint fix

---------

Co-authored-by: Bart Kalisz <bartlomiej.kalisz@gmail.com>
Co-authored-by: Manish Menaria <the.manish.menaria@gmail.com>
This commit is contained in:
Karol Manijak 2024-07-24 11:51:53 +02:00 committed by GitHub
parent e6d3890bdd
commit dace7ba296
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 289 additions and 166 deletions

View File

@ -32,7 +32,7 @@ const attributes = {
perPage: 5, perPage: 5,
pages: 1, pages: 1,
}, },
hideControls: [ CoreFilterNames.ORDER ], hideControls: [ CoreFilterNames.ORDER, CoreFilterNames.FILTERABLE ],
}; };
const heading: InnerBlockTemplate = [ const heading: InnerBlockTemplate = [

View File

@ -31,7 +31,7 @@ const attributes = {
perPage: 5, perPage: 5,
pages: 1, pages: 1,
}, },
hideControls: [ CoreFilterNames.FEATURED ], hideControls: [ CoreFilterNames.FEATURED, CoreFilterNames.FILTERABLE ],
}; };
const heading: InnerBlockTemplate = [ const heading: InnerBlockTemplate = [

View File

@ -40,7 +40,7 @@ const attributes = {
value: '-7 days', value: '-7 days',
}, },
}, },
hideControls: [ CoreFilterNames.ORDER ], hideControls: [ CoreFilterNames.ORDER, CoreFilterNames.FILTERABLE ],
}; };
const heading: InnerBlockTemplate = [ const heading: InnerBlockTemplate = [

View File

@ -34,7 +34,7 @@ const attributes = {
perPage: 5, perPage: 5,
pages: 1, pages: 1,
}, },
hideControls: [ CoreFilterNames.ON_SALE ], hideControls: [ CoreFilterNames.ON_SALE, CoreFilterNames.FILTERABLE ],
}; };
const heading: InnerBlockTemplate = [ const heading: InnerBlockTemplate = [

View File

@ -35,7 +35,7 @@ const attributes = {
perPage: 5, perPage: 5,
pages: 1, pages: 1,
}, },
hideControls: [ CoreFilterNames.ORDER ], hideControls: [ CoreFilterNames.ORDER, CoreFilterNames.FILTERABLE ],
}; };
const heading: InnerBlockTemplate = [ const heading: InnerBlockTemplate = [

View File

@ -62,6 +62,7 @@ export const DEFAULT_QUERY: ProductCollectionQuery = {
woocommerceHandPickedProducts: [], woocommerceHandPickedProducts: [],
timeFrame: undefined, timeFrame: undefined,
priceRange: undefined, priceRange: undefined,
filterable: false,
}; };
export const DEFAULT_ATTRIBUTES: Pick< export const DEFAULT_ATTRIBUTES: Pick<

View File

@ -36,7 +36,10 @@ import {
import { setQueryAttribute, getDefaultSettings } from '../../utils'; import { setQueryAttribute, getDefaultSettings } from '../../utils';
import UpgradeNotice from './upgrade-notice'; import UpgradeNotice from './upgrade-notice';
import ColumnsControl from './columns-control'; import ColumnsControl from './columns-control';
import InheritQueryControl from './inherit-query-control'; import {
InheritQueryControl,
FilterableControl,
} from './use-page-context-control';
import OrderByControl from './order-by-control'; import OrderByControl from './order-by-control';
import OnSaleControl from './on-sale-control'; import OnSaleControl from './on-sale-control';
import StockStatusControl from './stock-status-control'; import StockStatusControl from './stock-status-control';
@ -79,6 +82,8 @@ const ProductCollectionInspectorControls = (
const showQueryControls = inherit === false; const showQueryControls = inherit === false;
const showInheritQueryControl = const showInheritQueryControl =
isArchiveTemplate && shouldShowFilter( CoreFilterNames.INHERIT ); isArchiveTemplate && shouldShowFilter( CoreFilterNames.INHERIT );
const showFilterableControl =
! isArchiveTemplate && shouldShowFilter( CoreFilterNames.FILTERABLE );
const showOrderControl = const showOrderControl =
showQueryControls && shouldShowFilter( CoreFilterNames.ORDER ); showQueryControls && shouldShowFilter( CoreFilterNames.ORDER );
const showOnSaleControl = shouldShowFilter( CoreFilterNames.ON_SALE ); const showOnSaleControl = shouldShowFilter( CoreFilterNames.ON_SALE );
@ -129,6 +134,9 @@ const ProductCollectionInspectorControls = (
{ showInheritQueryControl && ( { showInheritQueryControl && (
<InheritQueryControl { ...queryControlProps } /> <InheritQueryControl { ...queryControlProps } />
) } ) }
{ showFilterableControl && (
<FilterableControl { ...queryControlProps } />
) }
<LayoutOptionsControl { ...displayControlProps } /> <LayoutOptionsControl { ...displayControlProps } />
<ColumnsControl { ...displayControlProps } /> <ColumnsControl { ...displayControlProps } />
{ showOrderControl && ( { showOrderControl && (

View File

@ -3,7 +3,6 @@
*/ */
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { usePrevious } from '@woocommerce/base-hooks'; import { usePrevious } from '@woocommerce/base-hooks';
import { select } from '@wordpress/data';
import { useMemo } from '@wordpress/element'; import { useMemo } from '@wordpress/element';
import { import {
ToggleControl, ToggleControl,
@ -17,62 +16,28 @@ import {
*/ */
import { import {
CoreFilterNames, CoreFilterNames,
ProductCollectionQuery, type ProductCollectionQuery,
QueryControlProps, type QueryControlProps,
} from '../../types'; } from '../../types';
import { DEFAULT_QUERY } from '../../constants'; import { DEFAULT_QUERY } from '../../constants';
import { getDefaultValueOfInheritQueryFromTemplate } from '../../utils'; import {
getDefaultValueOfInherit,
getDefaultValueOfFilterable,
} from '../../utils';
const label = __( 'Sync with current query', 'woocommerce' ); const label = __( 'Use page context', 'woocommerce' );
const productArchiveHelpText = __( const helpText = __(
'Enable to adjust the displayed products based on the current template and any applied filters.', 'Adjust the displayed products depending on the current template and any applied query filters.',
'woocommerce' 'woocommerce'
); );
const productsByCategoryHelpText = __(
'Enable to adjust the displayed products based on the current category and any applied filters.',
'woocommerce'
);
const productsByTagHelpText = __(
'Enable to adjust the displayed products based on the current tag and any applied filters.',
'woocommerce'
);
const productsByAttributeHelpText = __(
'Enable to adjust the displayed products based on the current attribute and any applied filters.',
'woocommerce'
);
const searchResultsHelpText = __(
'Enable to adjust the displayed products based on the current search and any applied filters.',
'woocommerce'
);
const getHelpTextForTemplate = ( templateId: string ): string => {
if ( templateId.includes( '//taxonomy-product_cat' ) ) {
return productsByCategoryHelpText;
}
if ( templateId.includes( '//taxonomy-product_tag' ) ) {
return productsByTagHelpText;
}
if ( templateId.includes( '//taxonomy-product_attribute' ) ) {
return productsByAttributeHelpText;
}
if ( templateId.includes( '//product-search-results' ) ) {
return searchResultsHelpText;
}
return productArchiveHelpText;
};
const InheritQueryControl = ( { const InheritQueryControl = ( {
setQueryAttribute, setQueryAttribute,
trackInteraction, trackInteraction,
query, query,
}: QueryControlProps ) => { }: QueryControlProps ) => {
const inherit = query?.inherit; const inherit = query?.inherit;
const editSiteStore = select( 'core/edit-site' );
const queryObjectBeforeInheritEnabled = usePrevious( const queryObjectBeforeInheritEnabled = usePrevious(
query, query,
@ -81,13 +46,7 @@ const InheritQueryControl = ( {
} }
); );
const defaultValue = useMemo( const defaultValue = useMemo( () => getDefaultValueOfInherit(), [] );
() => getDefaultValueOfInheritQueryFromTemplate(),
[]
);
const currentTemplateId = editSiteStore.getEditedPostId() as string;
const helpText = getHelpTextForTemplate( currentTemplateId );
return ( return (
<ToolsPanelItem <ToolsPanelItem
@ -128,4 +87,41 @@ const InheritQueryControl = ( {
); );
}; };
export default InheritQueryControl; const FilterableControl = ( {
setQueryAttribute,
trackInteraction,
query,
}: QueryControlProps ) => {
const filterable = query?.filterable;
const defaultValue = useMemo( () => getDefaultValueOfFilterable(), [] );
return (
<ToolsPanelItem
label={ label }
hasValue={ () => filterable !== defaultValue }
isShownByDefault
onDeselect={ () => {
setQueryAttribute( {
filterable: defaultValue,
} );
trackInteraction( CoreFilterNames.FILTERABLE );
} }
>
<ToggleControl
className="wc-block-product-collection__inherit-query-control"
label={ label }
help={ helpText }
checked={ !! filterable }
onChange={ ( value ) => {
setQueryAttribute( {
filterable: value,
} );
trackInteraction( CoreFilterNames.FILTERABLE );
} }
/>
</ToolsPanelItem>
);
};
export { FilterableControl, InheritQueryControl };

View File

@ -23,7 +23,8 @@ import type {
} from '../types'; } from '../types';
import { DEFAULT_ATTRIBUTES, INNER_BLOCKS_TEMPLATE } from '../constants'; import { DEFAULT_ATTRIBUTES, INNER_BLOCKS_TEMPLATE } from '../constants';
import { import {
getDefaultValueOfInheritQueryFromTemplate, getDefaultValueOfInherit,
getDefaultValueOfFilterable,
useSetPreviewState, useSetPreviewState,
} from '../utils'; } from '../utils';
import InspectorControls from './inspector-controls'; import InspectorControls from './inspector-controls';
@ -95,7 +96,8 @@ const ProductCollectionContent = ( {
...DEFAULT_ATTRIBUTES, ...DEFAULT_ATTRIBUTES,
query: { query: {
...( DEFAULT_ATTRIBUTES.query as ProductCollectionQuery ), ...( DEFAULT_ATTRIBUTES.query as ProductCollectionQuery ),
inherit: getDefaultValueOfInheritQueryFromTemplate(), inherit: getDefaultValueOfInherit(),
filterable: getDefaultValueOfFilterable(),
}, },
...( attributes as Partial< ProductCollectionAttributes > ), ...( attributes as Partial< ProductCollectionAttributes > ),
queryId, queryId,

View File

@ -28,6 +28,7 @@ export interface ProductCollectionAttributes {
*/ */
queryContextIncludes: string[]; queryContextIncludes: string[];
forcePageReload: boolean; forcePageReload: boolean;
filterable: boolean;
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
__privatePreviewState?: PreviewState; __privatePreviewState?: PreviewState;
} }
@ -93,6 +94,7 @@ export interface ProductCollectionQuery {
isProductCollectionBlock: boolean; isProductCollectionBlock: boolean;
woocommerceHandPickedProducts: string[]; woocommerceHandPickedProducts: string[];
priceRange: undefined | PriceRange; priceRange: undefined | PriceRange;
filterable: boolean;
} }
export type ProductCollectionEditComponentProps = export type ProductCollectionEditComponentProps =
@ -118,13 +120,15 @@ export type ProductCollectionSetAttributes = (
attrs: Partial< ProductCollectionAttributes > attrs: Partial< ProductCollectionAttributes >
) => void; ) => void;
export type TrackInteraction = ( filter: CoreFilterNames | string ) => void;
export type DisplayLayoutControlProps = { export type DisplayLayoutControlProps = {
displayLayout: ProductCollectionDisplayLayout; displayLayout: ProductCollectionDisplayLayout;
setAttributes: ProductCollectionSetAttributes; setAttributes: ProductCollectionSetAttributes;
}; };
export type QueryControlProps = { export type QueryControlProps = {
query: ProductCollectionQuery; query: ProductCollectionQuery;
trackInteraction: ( filter: CoreFilterNames | string ) => void; trackInteraction: TrackInteraction;
setQueryAttribute: ( attrs: Partial< ProductCollectionQuery > ) => void; setQueryAttribute: ( attrs: Partial< ProductCollectionQuery > ) => void;
}; };
@ -150,6 +154,7 @@ export enum CoreFilterNames {
STOCK_STATUS = 'stock-status', STOCK_STATUS = 'stock-status',
TAXONOMY = 'taxonomy', TAXONOMY = 'taxonomy',
PRICE_RANGE = 'price-range', PRICE_RANGE = 'price-range',
FILTERABLE = 'filterable',
} }
export type CollectionName = CoreCollectionNames | string; export type CollectionName = CoreCollectionNames | string;

View File

@ -80,7 +80,9 @@ const isInProductArchive = () => {
: false; : false;
}; };
const isFirstBlockThatSyncsWithQuery = () => { const isFirstBlockThatUsesPageContext = (
property: 'inherit' | 'filterable'
) => {
// We use experimental selector because it's been graduated as stable (`getBlocksByName`) // We use experimental selector because it's been graduated as stable (`getBlocksByName`)
// in Gutenberg 17.6 (https://github.com/WordPress/gutenberg/pull/58156) and will be // in Gutenberg 17.6 (https://github.com/WordPress/gutenberg/pull/58156) and will be
// available in WordPress 6.5. // available in WordPress 6.5.
@ -97,15 +99,23 @@ const isFirstBlockThatSyncsWithQuery = () => {
( clientId ) => { ( clientId ) => {
const block = getBlock( clientId ); const block = getBlock( clientId );
return block.attributes?.query?.inherit; return block.attributes?.query?.[ property ];
} }
); );
return ! blockAlreadySyncedWithQuery; return ! blockAlreadySyncedWithQuery;
}; };
export function getDefaultValueOfInheritQueryFromTemplate() { export function getDefaultValueOfInherit() {
return isInProductArchive() ? isFirstBlockThatSyncsWithQuery() : false; return isInProductArchive()
? isFirstBlockThatUsesPageContext( 'inherit' )
: false;
}
export function getDefaultValueOfFilterable() {
return ! isInProductArchive()
? isFirstBlockThatUsesPageContext( 'filterable' )
: false;
} }
/** /**
@ -226,7 +236,8 @@ export const getDefaultQuery = (
...currentQuery, ...currentQuery,
orderBy: DEFAULT_QUERY.orderBy as TProductCollectionOrderBy, orderBy: DEFAULT_QUERY.orderBy as TProductCollectionOrderBy,
order: DEFAULT_QUERY.order as TProductCollectionOrder, order: DEFAULT_QUERY.order as TProductCollectionOrder,
inherit: getDefaultValueOfInheritQueryFromTemplate(), inherit: getDefaultValueOfInherit(),
filterable: getDefaultValueOfFilterable(),
} ); } );
export const getDefaultDisplayLayout = () => export const getDefaultDisplayLayout = () =>
@ -246,7 +257,8 @@ export const getDefaultProductCollection = () =>
...DEFAULT_ATTRIBUTES, ...DEFAULT_ATTRIBUTES,
query: { query: {
...DEFAULT_ATTRIBUTES.query, ...DEFAULT_ATTRIBUTES.query,
inherit: getDefaultValueOfInheritQueryFromTemplate(), inherit: getDefaultValueOfInherit(),
filterable: getDefaultValueOfFilterable(),
}, },
}, },
createBlocksFromInnerBlocksTemplate( INNER_BLOCKS_TEMPLATE ) createBlocksFromInnerBlocksTemplate( INNER_BLOCKS_TEMPLATE )

View File

@ -126,14 +126,12 @@ test.describe( 'Product Collection', () => {
} ); } );
} ); } );
test.describe( 'Product Collection Sidebar Settings', () => { test.describe( 'Inspector Controls', () => {
test.beforeEach( async ( { pageObject } ) => {
await pageObject.createNewPostAndInsertBlock();
} );
test( 'Reflects the correct number of columns according to sidebar settings', async ( { test( 'Reflects the correct number of columns according to sidebar settings', async ( {
pageObject, pageObject,
} ) => { } ) => {
await pageObject.createNewPostAndInsertBlock();
await pageObject.setNumberOfColumns( 2 ); await pageObject.setNumberOfColumns( 2 );
await expect( pageObject.productTemplate ).toHaveClass( await expect( pageObject.productTemplate ).toHaveClass(
/columns-2/ /columns-2/
@ -154,6 +152,8 @@ test.describe( 'Product Collection', () => {
test( 'Order By - sort products by title in descending order correctly', async ( { test( 'Order By - sort products by title in descending order correctly', async ( {
pageObject, pageObject,
} ) => { } ) => {
await pageObject.createNewPostAndInsertBlock();
const sortedTitles = [ const sortedTitles = [
'WordPress Pennant', 'WordPress Pennant',
'V-Neck T-Shirt', 'V-Neck T-Shirt',
@ -177,6 +177,8 @@ test.describe( 'Product Collection', () => {
test( 'Products can be filtered based on "on sale" status', async ( { test( 'Products can be filtered based on "on sale" status', async ( {
pageObject, pageObject,
} ) => { } ) => {
await pageObject.createNewPostAndInsertBlock();
const allProducts = pageObject.products; const allProducts = pageObject.products;
const salePoducts = pageObject.products.filter( { const salePoducts = pageObject.products.filter( {
hasText: 'Product on sale', hasText: 'Product on sale',
@ -201,6 +203,8 @@ test.describe( 'Product Collection', () => {
test( 'Products can be filtered based on selection in handpicked products option', async ( { test( 'Products can be filtered based on selection in handpicked products option', async ( {
pageObject, pageObject,
} ) => { } ) => {
await pageObject.createNewPostAndInsertBlock();
await pageObject.addFilter( 'Show Hand-picked Products' ); await pageObject.addFilter( 'Show Hand-picked Products' );
const filterName = 'Hand-picked Products'; const filterName = 'Hand-picked Products';
@ -221,6 +225,7 @@ test.describe( 'Product Collection', () => {
pageObject, pageObject,
} ) => { } ) => {
await pageObject.createNewPostAndInsertBlock(); await pageObject.createNewPostAndInsertBlock();
await pageObject.addFilter( 'Keyword' ); await pageObject.addFilter( 'Keyword' );
await pageObject.setKeyword( 'Album' ); await pageObject.setKeyword( 'Album' );
@ -236,6 +241,8 @@ test.describe( 'Product Collection', () => {
test( 'Products can be filtered based on category.', async ( { test( 'Products can be filtered based on category.', async ( {
pageObject, pageObject,
} ) => { } ) => {
await pageObject.createNewPostAndInsertBlock();
const filterName = 'Product categories'; const filterName = 'Product categories';
await pageObject.addFilter( 'Show product categories' ); await pageObject.addFilter( 'Show product categories' );
await pageObject.setFilterComboboxValue( filterName, [ await pageObject.setFilterComboboxValue( filterName, [
@ -276,6 +283,8 @@ test.describe( 'Product Collection', () => {
test( 'Products can be filtered based on tags.', async ( { test( 'Products can be filtered based on tags.', async ( {
pageObject, pageObject,
} ) => { } ) => {
await pageObject.createNewPostAndInsertBlock();
const filterName = 'Product tags'; const filterName = 'Product tags';
await pageObject.addFilter( 'Show product tags' ); await pageObject.addFilter( 'Show product tags' );
await pageObject.setFilterComboboxValue( filterName, [ await pageObject.setFilterComboboxValue( filterName, [
@ -296,6 +305,8 @@ test.describe( 'Product Collection', () => {
test( 'Products can be filtered based on product attributes like color, size etc.', async ( { test( 'Products can be filtered based on product attributes like color, size etc.', async ( {
pageObject, pageObject,
} ) => { } ) => {
await pageObject.createNewPostAndInsertBlock();
await pageObject.addFilter( 'Show Product Attributes' ); await pageObject.addFilter( 'Show Product Attributes' );
await pageObject.setProductAttribute( 'Color', 'Green' ); await pageObject.setProductAttribute( 'Color', 'Green' );
@ -313,6 +324,8 @@ test.describe( 'Product Collection', () => {
test( 'Products can be filtered based on stock status (in stock, out of stock, or backorder).', async ( { test( 'Products can be filtered based on stock status (in stock, out of stock, or backorder).', async ( {
pageObject, pageObject,
} ) => { } ) => {
await pageObject.createNewPostAndInsertBlock();
await pageObject.setFilterComboboxValue( 'Stock status', [ await pageObject.setFilterComboboxValue( 'Stock status', [
'Out of stock', 'Out of stock',
] ); ] );
@ -331,6 +344,8 @@ test.describe( 'Product Collection', () => {
test( 'Products can be filtered based on featured status.', async ( { test( 'Products can be filtered based on featured status.', async ( {
pageObject, pageObject,
} ) => { } ) => {
await pageObject.createNewPostAndInsertBlock();
await expect( pageObject.products ).toHaveCount( 9 ); await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.addFilter( 'Featured' ); await pageObject.addFilter( 'Featured' );
@ -349,6 +364,8 @@ test.describe( 'Product Collection', () => {
test( 'Products can be filtered based on created date.', async ( { test( 'Products can be filtered based on created date.', async ( {
pageObject, pageObject,
} ) => { } ) => {
await pageObject.createNewPostAndInsertBlock();
await expect( pageObject.products ).toHaveCount( 9 ); await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.addFilter( 'Created' ); await pageObject.addFilter( 'Created' );
@ -376,6 +393,8 @@ test.describe( 'Product Collection', () => {
test( 'Products can be filtered based on price range.', async ( { test( 'Products can be filtered based on price range.', async ( {
pageObject, pageObject,
} ) => { } ) => {
await pageObject.createNewPostAndInsertBlock();
await expect( pageObject.products ).toHaveCount( 9 ); await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.addFilter( 'Price Range' ); await pageObject.addFilter( 'Price Range' );
@ -403,74 +422,62 @@ test.describe( 'Product Collection', () => {
await expect( pageObject.products ).toHaveCount( 4 ); await expect( pageObject.products ).toHaveCount( 4 );
} ); } );
test.describe( 'Sync with current template', () => { test.describe( '"Use page context" control', () => {
test( 'should not be visible on posts', async ( { test( 'should be visible on posts', async ( { pageObject } ) => {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock(); await pageObject.createNewPostAndInsertBlock();
const sidebarSettings =
await pageObject.locateSidebarSettings();
await expect( await expect(
sidebarSettings.locator( pageObject
SELECTORS.inheritQueryFromTemplateControl .locateSidebarSettings()
) .locator( SELECTORS.usePageContextControl )
).toBeHidden(); ).toBeVisible();
} ); } );
const archiveTemplates = [ [
'woocommerce/woocommerce//archive-product', 'woocommerce/woocommerce//archive-product',
'woocommerce/woocommerce//taxonomy-product_cat', 'woocommerce/woocommerce//taxonomy-product_cat',
'woocommerce/woocommerce//taxonomy-product_tag', 'woocommerce/woocommerce//taxonomy-product_tag',
'woocommerce/woocommerce//taxonomy-product_attribute', 'woocommerce/woocommerce//taxonomy-product_attribute',
'woocommerce/woocommerce//product-search-results', 'woocommerce/woocommerce//product-search-results',
]; ].forEach( ( slug ) => {
test( `should be visible in archive template: ${ slug }`, async ( {
const nonArchiveTemplates = [
'woocommerce/woocommerce//single-product',
'twentytwentyfour//home',
'twentytwentyfour//index',
];
archiveTemplates.map( async ( template ) => {
test( `should be visible in archive template: ${ template }`, async ( {
pageObject, pageObject,
editor, editor,
} ) => { } ) => {
await pageObject.goToEditorTemplate( template ); await pageObject.goToEditorTemplate( slug );
await pageObject.insertProductCollection(); await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate(); await pageObject.chooseCollectionInTemplate();
await pageObject.focusProductCollection(); await pageObject.focusProductCollection();
await editor.openDocumentSettingsSidebar(); await editor.openDocumentSettingsSidebar();
const sidebarSettings =
await pageObject.locateSidebarSettings();
await expect( await expect(
sidebarSettings.locator( pageObject
SELECTORS.inheritQueryFromTemplateControl .locateSidebarSettings()
) .locator( SELECTORS.usePageContextControl )
).toBeVisible(); ).toBeVisible();
} ); } );
} ); } );
nonArchiveTemplates.map( async ( template ) => { [
test( `should not be visible in non-archive template: ${ template }`, async ( { 'woocommerce/woocommerce//single-product',
'twentytwentyfour//home',
'twentytwentyfour//index',
].forEach( ( slug ) => {
test( `should be visible in non-archive template: ${ slug }`, async ( {
pageObject, pageObject,
editor, editor,
} ) => { } ) => {
await pageObject.goToEditorTemplate( template ); await pageObject.goToEditorTemplate( slug );
await pageObject.insertProductCollection(); await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate(); await pageObject.chooseCollectionInTemplate();
await pageObject.focusProductCollection(); await pageObject.focusProductCollection();
await editor.openDocumentSettingsSidebar(); await editor.openDocumentSettingsSidebar();
const sidebarSettings =
await pageObject.locateSidebarSettings();
await expect( await expect(
sidebarSettings.locator( pageObject
SELECTORS.inheritQueryFromTemplateControl .locateSidebarSettings()
) .locator( SELECTORS.usePageContextControl )
).toBeHidden(); ).toBeVisible();
} ); } );
} ); } );
@ -482,18 +489,15 @@ test.describe( 'Product Collection', () => {
await pageObject.focusProductCollection(); await pageObject.focusProductCollection();
await editor.openDocumentSettingsSidebar(); await editor.openDocumentSettingsSidebar();
const sidebarSettings = const sidebarSettings = pageObject.locateSidebarSettings();
await pageObject.locateSidebarSettings();
// Inherit query from template should be visible & enabled by default // Inherit query from template should be visible & enabled by default
await expect( await expect(
sidebarSettings.locator( sidebarSettings.locator( SELECTORS.usePageContextControl )
SELECTORS.inheritQueryFromTemplateControl
)
).toBeVisible(); ).toBeVisible();
await expect( await expect(
sidebarSettings.locator( sidebarSettings.locator(
`${ SELECTORS.inheritQueryFromTemplateControl } input` `${ SELECTORS.usePageContextControl } input`
) )
).toBeChecked(); ).toBeChecked();
@ -529,45 +533,136 @@ test.describe( 'Product Collection', () => {
).toBeChecked(); ).toBeChecked();
} ); } );
test( 'is enabled by default in 1st Product Collection and disabled in 2nd+', async ( { test( 'is enabled by default unless already enabled elsewhere', async ( {
pageObject, pageObject,
editor, editor,
} ) => { } ) => {
const productCollection = editor.canvas.getByLabel(
'Block: Product Collection',
{ exact: true }
);
const usePageContextToggle = pageObject
.locateSidebarSettings()
.locator( SELECTORS.usePageContextControl )
.locator( 'input' );
// First Product Catalog // First Product Catalog
// Option should be visible & ENABLED by default // Option should be visible & ENABLED by default
await pageObject.goToEditorTemplate(); await pageObject.goToEditorTemplate();
await pageObject.focusProductCollection(); await editor.selectBlocks( productCollection.first() );
await editor.openDocumentSettingsSidebar(); await editor.openDocumentSettingsSidebar();
const sidebarSettings = await expect( usePageContextToggle ).toBeChecked();
await pageObject.locateSidebarSettings();
await expect(
sidebarSettings.locator(
SELECTORS.inheritQueryFromTemplateControl
)
).toBeVisible();
await expect(
sidebarSettings.locator(
`${ SELECTORS.inheritQueryFromTemplateControl } input`
)
).toBeChecked();
// Second Product Catalog // Second Product Catalog
// Option should be visible & DISABLED by default // Option should be visible & DISABLED by default
await pageObject.insertProductCollection(); await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate( 'productCatalog' ); await pageObject.chooseCollectionInTemplate( 'productCatalog' );
await editor.selectBlocks( productCollection.last() );
await expect( usePageContextToggle ).not.toBeChecked();
// Disable the option in the first Product Catalog
await editor.selectBlocks( productCollection.first() );
await usePageContextToggle.click();
// Third Product Catalog
// Option should be visible & ENABLED by default
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate( 'productCatalog' );
await editor.selectBlocks( productCollection.last() );
await expect( usePageContextToggle ).toBeChecked();
} );
test( 'allows filtering in non-archive context', async ( {
pageObject,
editor,
page,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInPost( 'productCatalog' );
await expect( pageObject.products ).toHaveCount( 18 );
await page.getByLabel( 'Toggle block inserter' ).click();
await page.getByRole( 'tab', { name: 'Patterns' } ).click();
await page
.getByPlaceholder( 'Search' )
.fill( 'product filters' );
await page.getByLabel( 'Product Filters' ).click();
const postId = await editor.publishPost();
await page.goto( `/?p=${ postId }` );
const productCollection = page.locator(
'.wp-block-woocommerce-product-collection'
);
await expect( await expect(
sidebarSettings.locator( productCollection.first().locator( SELECTORS.product )
SELECTORS.inheritQueryFromTemplateControl ).toHaveCount( 9 );
)
).toBeVisible();
await expect( await expect(
sidebarSettings.locator( productCollection.last().locator( SELECTORS.product )
`${ SELECTORS.inheritQueryFromTemplateControl } input` ).toHaveCount( 9 );
)
).not.toBeChecked(); await page
.getByRole( 'textbox', {
name: 'Filter products by maximum',
} )
.dblclick();
await page.keyboard.type( '10' );
await page.keyboard.press( 'Tab' );
await expect(
productCollection.first().locator( SELECTORS.product )
).toHaveCount( 1 );
await expect(
productCollection.last().locator( SELECTORS.product )
).toHaveCount( 9 );
} );
test( 'correctly combines editor and front-end filters', async ( {
pageObject,
editor,
page,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.addFilter( 'Show product categories' );
await pageObject.setFilterComboboxValue( 'Product categories', [
'Music',
] );
await page.getByLabel( 'Toggle block inserter' ).click();
await page.getByRole( 'tab', { name: 'Patterns' } ).click();
await page
.getByPlaceholder( 'Search' )
.fill( 'product filters' );
await page.getByLabel( 'Product Filters' ).click();
await expect( pageObject.products ).toHaveCount( 2 );
const postId = await editor.publishPost();
await page.goto( `/?p=${ postId }` );
await expect( pageObject.products ).toHaveCount( 2 );
await page
.getByRole( 'textbox', {
name: 'Filter products by maximum',
} )
.dblclick();
await page.keyboard.type( '5' );
await page.keyboard.press( 'Tab' );
await expect( pageObject.products ).toHaveCount( 1 );
} ); } );
} ); } );
} ); } );
@ -787,17 +882,16 @@ test.describe( 'Product Collection', () => {
await expect( pageObject.products ).toHaveCount( 4 ); await expect( pageObject.products ).toHaveCount( 4 );
} ); } );
test( "Product Catalog Collection can be added in post and doesn't sync query with template", async ( { test( 'Product Catalog Collection can be added in post and syncs query with template', async ( {
pageObject, pageObject,
} ) => { } ) => {
await pageObject.createNewPostAndInsertBlock( 'productCatalog' ); await pageObject.createNewPostAndInsertBlock( 'productCatalog' );
const sidebarSettings = await pageObject.locateSidebarSettings(); const usePageContextToggle = pageObject
const input = sidebarSettings.locator( .locateSidebarSettings()
`${ SELECTORS.inheritQueryFromTemplateControl } input` .locator( `${ SELECTORS.usePageContextControl } input` );
);
await expect( input ).toBeHidden(); await expect( usePageContextToggle ).toBeVisible();
await expect( pageObject.products ).toHaveCount( 9 ); await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.publishAndGoToFrontend(); await pageObject.publishAndGoToFrontend();
@ -822,9 +916,9 @@ test.describe( 'Product Collection', () => {
await pageObject.chooseCollectionInTemplate(); await pageObject.chooseCollectionInTemplate();
await editor.openDocumentSettingsSidebar(); await editor.openDocumentSettingsSidebar();
const sidebarSettings = await pageObject.locateSidebarSettings(); const sidebarSettings = pageObject.locateSidebarSettings();
const input = sidebarSettings.locator( const input = sidebarSettings.locator(
`${ SELECTORS.inheritQueryFromTemplateControl } input` `${ SELECTORS.usePageContextControl } input`
); );
await expect( input ).toBeChecked(); await expect( input ).toBeChecked();
@ -854,8 +948,7 @@ test.describe( 'Product Collection', () => {
test( 'On Sale', async ( { pageObject } ) => { test( 'On Sale', async ( { pageObject } ) => {
await pageObject.createNewPostAndInsertBlock( 'onSale' ); await pageObject.createNewPostAndInsertBlock( 'onSale' );
const sidebarSettings = const sidebarSettings = pageObject.locateSidebarSettings();
await pageObject.locateSidebarSettings();
const input = sidebarSettings.getByLabel( const input = sidebarSettings.getByLabel(
SELECTORS.onSaleControlLabel SELECTORS.onSaleControlLabel
); );
@ -865,8 +958,7 @@ test.describe( 'Product Collection', () => {
test( 'Featured', async ( { pageObject } ) => { test( 'Featured', async ( { pageObject } ) => {
await pageObject.createNewPostAndInsertBlock( 'featured' ); await pageObject.createNewPostAndInsertBlock( 'featured' );
const sidebarSettings = const sidebarSettings = pageObject.locateSidebarSettings();
await pageObject.locateSidebarSettings();
const input = sidebarSettings.getByLabel( const input = sidebarSettings.getByLabel(
SELECTORS.featuredControlLabel SELECTORS.featuredControlLabel
); );
@ -1477,7 +1569,7 @@ test.describe( 'Testing registerProductCollection', () => {
'myCustomCollection' 'myCustomCollection'
); );
const sidebarSettings = await pageObject.locateSidebarSettings(); const sidebarSettings = pageObject.locateSidebarSettings();
const onsaleControl = sidebarSettings.getByLabel( const onsaleControl = sidebarSettings.getByLabel(
SELECTORS.onSaleControlLabel SELECTORS.onSaleControlLabel
); );

View File

@ -38,7 +38,7 @@ export const SELECTORS = {
}, },
onSaleControlLabel: 'Show only products on sale', onSaleControlLabel: 'Show only products on sale',
featuredControlLabel: 'Show only featured products', featuredControlLabel: 'Show only featured products',
inheritQueryFromTemplateControl: usePageContextControl:
'.wc-block-product-collection__inherit-query-control', '.wc-block-product-collection__inherit-query-control',
shrinkColumnsToFit: 'Responsive', shrinkColumnsToFit: 'Responsive',
productSearchLabel: 'Search', productSearchLabel: 'Search',
@ -374,7 +374,7 @@ class ProductCollectionPage {
} }
async setNumberOfColumns( numberOfColumns: number ) { async setNumberOfColumns( numberOfColumns: number ) {
const sidebarSettings = await this.locateSidebarSettings(); const sidebarSettings = this.locateSidebarSettings();
const inputField = sidebarSettings.getByRole( 'spinbutton', { const inputField = sidebarSettings.getByRole( 'spinbutton', {
name: 'Columns', name: 'Columns',
} ); } );
@ -390,7 +390,7 @@ class ProductCollectionPage {
| 'popularity/desc' | 'popularity/desc'
| 'rating/desc' | 'rating/desc'
) { ) {
const sidebarSettings = await this.locateSidebarSettings(); const sidebarSettings = this.locateSidebarSettings();
const orderByComboBox = sidebarSettings.getByRole( 'combobox', { const orderByComboBox = sidebarSettings.getByRole( 'combobox', {
name: 'Order by', name: 'Order by',
} ); } );
@ -400,7 +400,7 @@ class ProductCollectionPage {
} }
async getOrderByElement() { async getOrderByElement() {
const sidebarSettings = await this.locateSidebarSettings(); const sidebarSettings = this.locateSidebarSettings();
return sidebarSettings.getByRole( 'combobox', { return sidebarSettings.getByRole( 'combobox', {
name: 'Order by', name: 'Order by',
} ); } );
@ -423,7 +423,7 @@ class ProductCollectionPage {
onSale: true, onSale: true,
} }
) { ) {
const sidebarSettings = await this.locateSidebarSettings(); const sidebarSettings = this.locateSidebarSettings();
const input = sidebarSettings.getByLabel( const input = sidebarSettings.getByLabel(
SELECTORS.onSaleControlLabel SELECTORS.onSaleControlLabel
); );
@ -448,7 +448,7 @@ class ProductCollectionPage {
isLocatorsRefreshNeeded: true, isLocatorsRefreshNeeded: true,
} }
) { ) {
const sidebarSettings = await this.locateSidebarSettings(); const sidebarSettings = this.locateSidebarSettings();
const input = sidebarSettings.getByLabel( const input = sidebarSettings.getByLabel(
SELECTORS.featuredControlLabel SELECTORS.featuredControlLabel
); );
@ -475,7 +475,7 @@ class ProductCollectionPage {
const operatorSelector = SELECTORS.createdFilter.operator[ operator ]; const operatorSelector = SELECTORS.createdFilter.operator[ operator ];
const rangeSelector = SELECTORS.createdFilter.range[ range ]; const rangeSelector = SELECTORS.createdFilter.range[ range ];
const sidebarSettings = await this.locateSidebarSettings(); const sidebarSettings = this.locateSidebarSettings();
const operatorButton = sidebarSettings.getByLabel( operatorSelector ); const operatorButton = sidebarSettings.getByLabel( operatorSelector );
const rangeButton = sidebarSettings.getByLabel( rangeSelector ); const rangeButton = sidebarSettings.getByLabel( rangeSelector );
@ -487,7 +487,7 @@ class ProductCollectionPage {
const minInputSelector = SELECTORS.priceRangeFilter.min; const minInputSelector = SELECTORS.priceRangeFilter.min;
const maxInputSelector = SELECTORS.priceRangeFilter.max; const maxInputSelector = SELECTORS.priceRangeFilter.max;
const sidebarSettings = await this.locateSidebarSettings(); const sidebarSettings = this.locateSidebarSettings();
const minInput = sidebarSettings.getByLabel( minInputSelector ); const minInput = sidebarSettings.getByLabel( minInputSelector );
const maxInput = sidebarSettings.getByLabel( maxInputSelector ); const maxInput = sidebarSettings.getByLabel( maxInputSelector );
@ -498,7 +498,7 @@ class ProductCollectionPage {
} }
async setFilterComboboxValue( filterName: string, filterValue: string[] ) { async setFilterComboboxValue( filterName: string, filterValue: string[] ) {
const sidebarSettings = await this.locateSidebarSettings(); const sidebarSettings = this.locateSidebarSettings();
const input = sidebarSettings.getByLabel( filterName ); const input = sidebarSettings.getByLabel( filterName );
await input.click(); await input.click();
@ -527,7 +527,7 @@ class ProductCollectionPage {
} }
async setKeyword( keyword: string ) { async setKeyword( keyword: string ) {
const sidebarSettings = await this.locateSidebarSettings(); const sidebarSettings = this.locateSidebarSettings();
const input = sidebarSettings.getByLabel( 'Keyword' ); const input = sidebarSettings.getByLabel( 'Keyword' );
await input.clear(); await input.clear();
await input.fill( keyword ); await input.fill( keyword );
@ -590,7 +590,7 @@ class ProductCollectionPage {
} }
async setShrinkColumnsToFit( value = true ) { async setShrinkColumnsToFit( value = true ) {
const sidebarSettings = await this.locateSidebarSettings(); const sidebarSettings = this.locateSidebarSettings();
const input = sidebarSettings.getByLabel( const input = sidebarSettings.getByLabel(
SELECTORS.shrinkColumnsToFit SELECTORS.shrinkColumnsToFit
); );
@ -602,7 +602,7 @@ class ProductCollectionPage {
} }
async setProductAttribute( attribute: 'Color' | 'Size', value: string ) { async setProductAttribute( attribute: 'Color' | 'Size', value: string ) {
const sidebarSettings = await this.locateSidebarSettings(); const sidebarSettings = this.locateSidebarSettings();
const productAttributesContainer = sidebarSettings.locator( const productAttributesContainer = sidebarSettings.locator(
'.woocommerce-product-attributes' '.woocommerce-product-attributes'
@ -631,9 +631,9 @@ class ProductCollectionPage {
} }
async setInheritQueryFromTemplate( inheritQueryFromTemplate: boolean ) { async setInheritQueryFromTemplate( inheritQueryFromTemplate: boolean ) {
const sidebarSettings = await this.locateSidebarSettings(); const sidebarSettings = this.locateSidebarSettings();
const input = sidebarSettings.locator( const input = sidebarSettings.locator(
`${ SELECTORS.inheritQueryFromTemplateControl } input` `${ SELECTORS.usePageContextControl } input`
); );
if ( inheritQueryFromTemplate ) { if ( inheritQueryFromTemplate ) {
await input.check(); await input.check();
@ -687,7 +687,7 @@ class ProductCollectionPage {
/** /**
* Locators * Locators
*/ */
async locateSidebarSettings() { locateSidebarSettings() {
return this.page.getByRole( 'region', { return this.page.getByRole( 'region', {
name: 'Editor settings', name: 'Editor settings',
} ); } );

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Product Collection: Rename "Sync with current query" option to "Use page context" and make it working in non-archive context as well

View File

@ -524,7 +524,10 @@ class ProductCollection extends AbstractBlock {
// phpcs:ignore WordPress.DB.SlowDBQuery // phpcs:ignore WordPress.DB.SlowDBQuery
$block_context_query['tax_query'] = ! empty( $query['tax_query'] ) ? $query['tax_query'] : array(); $block_context_query['tax_query'] = ! empty( $query['tax_query'] ) ? $query['tax_query'] : array();
$is_exclude_applied_filters = ! ( $block->context['query']['inherit'] ?? false ); $inherit = $block->context['query']['inherit'] ?? false;
$filterable = $block->context['query']['filterable'] ?? false;
$is_exclude_applied_filters = ! ( $inherit || $filterable );
return $this->get_final_frontend_query( $block_context_query, $page, $is_exclude_applied_filters ); return $this->get_final_frontend_query( $block_context_query, $page, $is_exclude_applied_filters );
} }