Product Collection: Make attributes available in rest_product_query hook (#44150)

* Make attributes available in rest_product_query hook

This commit introduces the 'includeInQueryContext' attribute to the 'woocommerce/product-collection' block and updates the 'woocommerce/product-template' block to consume this new attribute.

Key Changes:
1. `woocommerce/product-collection` Block:
   - A new attribute 'includeInQueryContext' is added in `block.json`. This attribute is designed to hold a list of attribute names relevant for the query context.
   - The 'includeInQueryContext' attribute is included in the `providesContext` field to ensure its availability to child blocks.
   - In `constants.ts`, default values for 'includeInQueryContext' are defined, specifying 'collection' and 'id' as initial attributes.
   - The `types.ts` file is updated with a comment explaining the purpose of 'includeInQueryContext'.

2. `woocommerce/product-template` Block:
   - Modified `block.json` to utilize the 'includeInQueryContext' context provided by the parent `woocommerce/product-collection` block.
   - The `edit.tsx` file is updated to handle the new context. It uses a newly added utility function `useProductCollectionBlockAttributes` from `utils.tsx` to access parent block attributes.
   - The `utils.tsx` file is introduced, containing the `useProductCollectionBlockAttributes` hook. This hook is responsible for finding the parent 'woocommerce/product-collection' block and returning its attributes.
   - Within `edit.tsx`, logic is added to create a query context object based on the attributes specified in 'includeInQueryContext', enhancing the block's ability to dynamically adapt to changes.

* Remove commented code

* Rename query context attribute and optimize parent block detection

This commit introduces two significant changes aimed at improving code readability and efficiency.

1. **Renaming of Query Context Attribute:**
   - The attribute `includeInQueryContext` has been renamed to `queryContextIncludes` across various files, including block JSON configurations and TypeScript definitions. This change makes the attribute's purpose more intuitive, indicating it specifies which attributes to include in the query context.

2. **Optimized Parent Block Detection:**
   - Replaced the use of `getBlockParents` with `getBlockParentsByBlockName` in utility functions to find the closest Product Collection block. This optimization allows for a more direct and efficient way to identify the relevant parent block by specifying the block name, reducing unnecessary iterations and improving code performance.

* Streamline query context construction in product template

Key Changes:
- **Introduction of `useProductCollectionQueryContext` Hook:** This new hook takes the `clientId` and `queryContextIncludes` as inputs and returns a query context object. It encapsulates the logic for fetching parent product collection block attributes and constructing the query context accordingly. This abstraction simplifies the edit component's logic, focusing on the essentials and improving code readability.
- **Optimization of Parent Block Detection:** The hook uses `getBlockParentsByBlockName` to accurately and efficiently find the closest parent `Product Collection` block, minimizing the overhead previously associated with traversing the block hierarchy.

* Always include `collection` and `id` in query context

* Minor refactor

* Enhance query context handling for more maintainable code

- Introduced `DEFAULT_QUERY_CONTEXT_ATTRIBUTES` in `edit.tsx` to maintain a clear list of default query context attributes.
- Modified `ProductTemplateEdit` to automatically include these default attributes in `queryContextIncludes`, ensuring they are always part of the query context without manual initialization.
- Simplified `useProductCollectionQueryContext` in `utils.tsx` by removing static initialization of 'collection' and 'id', relying instead on the dynamic addition of necessary attributes from `queryContextIncludes`.

This refactor enhances the maintainability and clarity of the code, ensuring a solid foundation for future enhancements and features.

* Add E2E tests for Product Collection query context

- Added a new test suite 'Query Context in Editor' to validate the correctness of query context parameters when the Product Collection block is used. This suite ensures that:
  - For the 'Product Catalog', only the ID is sent in the query context, confirming that collection-specific parameters are excluded when not relevant.
  - For collections such as 'On Sale', the collection name is correctly passed in the query context, validating that the block dynamically adjusts query parameters based on its settings.

- Introduced a new utility method `setupAndFetchQueryContextURL` in `product-collection.page.ts`. This method automates the setup of a post with the Product Collection block and fetches the URL with query context parameters, facilitating the validation of query context handling.

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

* Fix edge case when `queryContextIncludes` is not defined

- Initializing `queryContextIncludes` with a default empty array directly in the destructuring assignment of the component's props. This approach ensures that `queryContextIncludes` is always an array, simplifying downstream logic.
- Creating a new constant `queryContextIncludesWithDefaults` to hold the combination of `queryContextIncludes` and `DEFAULT_QUERY_CONTEXT_ATTRIBUTES`. This step avoids directly mutating the `queryContextIncludes` prop, aligning with best practices for functional purity and making the code easier to understand and debug.
- Updating the `useProductCollectionQueryContext` hook call to use `queryContextIncludesWithDefaults`. This ensures that the default query context attributes are consistently included without altering the original prop.

These adjustments not only enhance the code's maintainability but also ensure more predictable behavior by avoiding side effects related to parameter mutation.

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Manish Menaria 2024-02-12 14:15:24 +05:30 committed by GitHub
parent 96f3e3c98b
commit 4ae60196ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 180 additions and 2 deletions

View File

@ -34,6 +34,9 @@
"hideControls": {
"default": [],
"type": "array"
},
"queryContextIncludes": {
"type": "array"
}
},
"providesContext": {
@ -41,6 +44,7 @@
"queryId": "queryId",
"query": "query",
"displayLayout": "displayLayout",
"queryContextIncludes": "queryContextIncludes",
"collection": "collection"
},
"supports": {

View File

@ -71,6 +71,7 @@ export const DEFAULT_ATTRIBUTES: Partial< ProductCollectionAttributes > = {
columns: 3,
shrinkColumns: true,
},
queryContextIncludes: [ 'collection', 'id' ],
};
export const getDefaultQuery = (

View File

@ -19,6 +19,10 @@ export interface ProductCollectionAttributes {
convertedFromProducts: boolean;
collection?: string;
hideControls: FilterName[];
/**
* Contain the list of attributes that should be included in the queryContext
*/
queryContextIncludes: string[];
}
export enum LayoutOptions {

View File

@ -14,6 +14,7 @@
"queryContext",
"displayLayout",
"templateSlug",
"queryContextIncludes",
"collection"
],
"supports": {

View File

@ -25,8 +25,11 @@ import type { BlockEditProps, BlockInstance } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { useProductCollectionQueryContext } from './utils';
import './editor.scss';
const DEFAULT_QUERY_CONTEXT_ATTRIBUTES = [ 'collection', 'id' ];
const ProductTemplateInnerBlocks = () => {
const innerBlocksProps = useInnerBlocksProps(
{ className: 'wc-block-product' },
@ -144,6 +147,7 @@ const ProductTemplateEdit = ( {
columns: 3,
shrinkColumns: false,
},
queryContextIncludes = [],
},
__unstableLayoutClassNames,
}: BlockEditProps< {
@ -161,6 +165,19 @@ const ProductTemplateEdit = ( {
12,
isNumber
);
// Add default query context attributes to queryContextIncludes
const queryContextIncludesWithDefaults = [
...new Set(
queryContextIncludes.concat( DEFAULT_QUERY_CONTEXT_ATTRIBUTES )
),
];
const productCollectionQueryContext = useProductCollectionQueryContext( {
clientId,
queryContextIncludes: queryContextIncludesWithDefaults,
} );
const { products, blocks } = useSelect(
( select ) => {
const { getEntityRecords, getTaxonomies } = select( coreStore );
@ -225,6 +242,7 @@ const ProductTemplateEdit = ( {
products: getEntityRecords( 'postType', postType, {
...query,
...restQueryArgs,
productCollectionQueryContext,
} ),
blocks: getBlocks( clientId ),
};
@ -243,6 +261,8 @@ const ProductTemplateEdit = ( {
templateSlug,
taxQuery,
restQueryArgs,
productCollectionQueryContext,
loopShopPerPage,
]
);
const blockContexts = useMemo(

View File

@ -0,0 +1,80 @@
/**
* External dependencies
*/
import { useSelect } from '@wordpress/data';
import { useMemo } from '@wordpress/element';
/**
* In Product Collection block, queryContextIncludes attribute contains
* list of attribute names that should be included in the query context.
*
* This hook returns the query context object based on the attribute names
* provided in the queryContextIncludes array.
*
* Example:
* {
* clientID = 'd2c7e34f-70d6-417c-b582-f554a3a575f3',
* queryContextIncludes = [ 'collection' ]
* }
*
* The hook will return the following query context object:
* {
* collection: 'woocommerce/product-collection/featured'
* }
*
* @param args Arguments for the hook.
* @param args.clientId Client ID of the inner block.
* @param args.queryContextIncludes Array of attribute names to be included in the query context.
*
* @return Query context object.
*/
export const useProductCollectionQueryContext = ( {
clientId,
queryContextIncludes,
}: {
clientId: string;
queryContextIncludes: string[];
} ) => {
const productCollectionBlockAttributes = useSelect(
( select ) => {
const { getBlockParentsByBlockName, getBlockAttributes } =
select( 'core/block-editor' );
const parentBlocksClientIds = getBlockParentsByBlockName(
clientId,
'woocommerce/product-collection',
true
);
if ( parentBlocksClientIds?.length ) {
const closestParentClientId = parentBlocksClientIds[ 0 ];
return getBlockAttributes( closestParentClientId );
}
return null;
},
[ clientId ]
);
return useMemo( () => {
// If the product collection block is not found, return null.
if ( ! productCollectionBlockAttributes ) {
return null;
}
const queryContext: {
[ key: string ]: unknown;
} = {};
if ( queryContextIncludes?.length ) {
queryContextIncludes.forEach( ( attribute: string ) => {
if ( productCollectionBlockAttributes?.[ attribute ] ) {
queryContext[ attribute ] =
productCollectionBlockAttributes[ attribute ];
}
} );
}
return queryContext;
}, [ queryContextIncludes, productCollectionBlockAttributes ] );
};

View File

@ -733,4 +733,42 @@ test.describe( 'Product Collection', () => {
);
} );
} );
test.describe( 'Query Context in Editor', () => {
test( 'Product Catalog: Sends only ID in Query Context', async ( {
pageObject,
} ) => {
const url = await pageObject.setupAndFetchQueryContextURL( {
collection: 'productCatalog',
} );
await expect(
url.searchParams.has( 'productCollectionQueryContext[id]' )
).toBeTruthy();
// There shouldn't be collection in the query context
// Because Product Catalog isn't a collection
await expect(
url.searchParams.has(
'productCollectionQueryContext[collection]'
)
).toBeFalsy();
} );
test( 'Collections: collection should be present in query context', async ( {
pageObject,
} ) => {
const url = await pageObject.setupAndFetchQueryContextURL( {
collection: 'onSale',
} );
const collectionName = url.searchParams.get(
'productCollectionQueryContext[collection]'
);
await expect( collectionName ).toBeTruthy();
await expect( collectionName ).toBe(
'woocommerce/product-collection/on-sale'
);
} );
} );
} );

View File

@ -134,6 +134,32 @@ class ProductCollectionPage {
await this.editor.openDocumentSettingsSidebar();
}
async setupAndFetchQueryContextURL( {
collection,
}: {
collection: Collections;
} ) {
await this.admin.createNewPost();
await this.editorUtils.closeWelcomeGuideModal();
await this.editor.insertBlock( {
name: this.BLOCK_NAME,
} );
await this.chooseCollectionInPost( collection );
// Wait for response with productCollectionQueryContext query parameter.
const WP_PRODUCT_ENDPOINT = '/wp/v2/product';
const QUERY_CONTEXT_PARAM = 'productCollectionQueryContext';
const response = await this.page.waitForResponse(
( currentResponse ) =>
currentResponse.url().includes( WP_PRODUCT_ENDPOINT ) &&
currentResponse.url().includes( QUERY_CONTEXT_PARAM ) &&
currentResponse.status() === 200
);
const url = new URL( response.url() );
return url;
}
async publishAndGoToFrontend() {
await this.editor.publishPost();
const url = new URL( this.page.url() );
@ -155,7 +181,7 @@ class ProductCollectionPage {
await this.editorUtils.enterEditMode();
await this.editorUtils.replaceBlockByBlockName(
'core/query',
'woocommerce/product-collection'
this.BLOCK_NAME
);
await this.chooseCollectionInTemplate( collection );
await this.editor.saveSiteEditorEntities();
@ -500,7 +526,7 @@ class ProductCollectionPage {
await this.editor.selectBlocks( siblingBlock );
await this.editorUtils.insertBlock(
{ name: 'woocommerce/product-collection' },
{ name: this.BLOCK_NAME },
undefined,
parentClientId
);

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Add `queryContextIncludes` attribute to Product Collection block