Fix: Ensure new filter blocks work with inherited query context (https://github.com/woocommerce/woocommerce-blocks/pull/12022)

* make the collection filters block available globally

* make query optional

* remove unnecessary editor contextual
This commit is contained in:
Tung Du 2023-12-04 21:05:09 +07:00 committed by GitHub
parent 945e476648
commit 2bc48e4e4a
12 changed files with 41 additions and 272 deletions

View File

@ -19,9 +19,6 @@
"providesContext": {
"collectionData": "collectionData"
},
"ancestor": [
"woocommerce/product-collection"
],
"attributes": {
"collectionData": {
"type": "object",

View File

@ -2,19 +2,10 @@
* External dependencies
*/
import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
import { useSelect } from '@wordpress/data';
import { useEffect } from '@wordpress/element';
import { useCollection } from '@woocommerce/base-context/hooks';
import { sprintf, __ } from '@wordpress/i18n';
import { getSetting } from '@woocommerce/settings';
import type { AttributeSetting } from '@woocommerce/types';
/**
* Internal dependencies
*/
import { formatQuery, getQueryParams } from './utils';
import type { EditProps } from './type';
const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
const template = [
@ -63,32 +54,12 @@ if ( firstAttribute ) {
);
}
const Edit = ( { clientId, setAttributes, context }: EditProps ) => {
const Edit = () => {
const blockProps = useBlockProps();
const innerBlockProps = useInnerBlocksProps( blockProps, {
template,
} );
// Get inner blocks by clientId
const currentBlock = useSelect( ( select ) => {
return select( 'core/block-editor' ).getBlock( clientId );
} );
const { results } = useCollection( {
namespace: '/wc/store/v1',
resourceName: 'products/collection-data',
query: {
...formatQuery( context.query ),
...getQueryParams( currentBlock ),
},
} );
useEffect( () => {
setAttributes( {
collectionData: results,
} );
}, [ results, setAttributes ] );
return <nav { ...innerBlockProps } />;
};

View File

@ -5,7 +5,10 @@ import { __ } from '@wordpress/i18n';
import { useEffect, useState } from '@wordpress/element';
import { BlockControls, useBlockProps } from '@wordpress/block-editor';
import { getSetting } from '@woocommerce/settings';
import { useCollection } from '@woocommerce/base-context/hooks';
import {
useCollection,
useCollectionData,
} from '@woocommerce/base-context/hooks';
import {
AttributeSetting,
AttributeTerm,
@ -41,7 +44,6 @@ const Edit = ( props: EditProps ) => {
attributes: blockAttributes,
setAttributes,
debouncedSpeak,
context,
} = props;
const {
@ -70,6 +72,15 @@ const Edit = ( props: EditProps ) => {
query: { orderby: 'menu_order' },
} );
const { results: filteredCounts } = useCollectionData( {
queryAttribute: {
taxonomy: attributeObject?.taxonomy || '',
queryType: blockAttributes.queryType,
},
queryState: {},
isEditor: true,
} );
const blockProps = useBlockProps();
useEffect( () => {
@ -103,11 +114,9 @@ const Edit = ( props: EditProps ) => {
useEffect( () => {
const termIdHasProducts =
objectHasProp( context.collectionData, 'attribute_counts' ) &&
isAttributeCounts( context.collectionData.attribute_counts )
? context.collectionData.attribute_counts.map(
( term ) => term.term
)
objectHasProp( filteredCounts, 'attribute_counts' ) &&
isAttributeCounts( filteredCounts.attribute_counts )
? filteredCounts.attribute_counts.map( ( term ) => term.term )
: [];
if ( termIdHasProducts.length === 0 ) return setAttributeOptions( [] );
@ -117,7 +126,7 @@ const Edit = ( props: EditProps ) => {
return termIdHasProducts.includes( term.id );
} )
);
}, [ attributeTerms, context.collectionData ] );
}, [ attributeTerms, filteredCounts ] );
const Toolbar = () => (
<BlockControls>

View File

@ -15,9 +15,6 @@ export type BlockAttributes = {
export interface EditProps extends BlockEditProps< BlockAttributes > {
debouncedSpeak: ( label: string ) => void;
context: {
collectionData: unknown[];
};
}
type AttributeCount = {

View File

@ -15,12 +15,9 @@ import {
/**
* Internal dependencies
*/
import type { FilterComponentProps } from '../types';
import type { EditProps } from '../types';
export const Inspector = ( {
attributes,
setAttributes,
}: Omit< FilterComponentProps, 'collectionData' > ) => {
export const Inspector = ( { attributes, setAttributes }: EditProps ) => {
const { showInputFields, inlineInput } = attributes;
return (

View File

@ -1,3 +1,8 @@
/**
* External dependencies
*/
import { useCollectionData } from '@woocommerce/base-context/hooks';
/**
* Internal dependencies
*/
@ -8,10 +13,19 @@ import { getFormattedPrice } from '../utils';
* We pass the whole props from Edit component to <PriceSlider/> so we're
* reusing the EditProps type here.
*/
export const PriceSlider = ( { attributes, context }: EditProps ) => {
export const PriceSlider = ( { attributes }: EditProps ) => {
const { showInputFields } = attributes;
const { results, isLoading } = useCollectionData( {
queryPrices: true,
queryState: {},
isEditor: true,
} );
if ( isLoading ) return null;
const { minPrice, maxPrice, formattedMinPrice, formattedMaxPrice } =
getFormattedPrice( context.collectionData );
getFormattedPrice( results );
const onChange = () => null;

View File

@ -8,11 +8,7 @@ export type BlockAttributes = {
inlineInput: boolean;
};
export interface EditProps extends BlockEditProps< BlockAttributes > {
context: {
collectionData: unknown[];
};
}
export type EditProps = BlockEditProps< BlockAttributes >;
export type PriceFilterState = {
minPrice?: number;
@ -23,7 +19,3 @@ export type PriceFilterState = {
formattedMinPrice: string;
formattedMaxPrice: string;
};
export type FilterComponentProps = BlockEditProps< BlockAttributes > & {
collectionData: Partial< PriceFilterState >;
};

View File

@ -7,16 +7,12 @@ import { useBlockProps } from '@wordpress/block-editor';
import { Disabled } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { Icon, chevronDown } from '@wordpress/icons';
import { ProductQueryContext as Context } from '@woocommerce/blocks/product-query/types';
import { CheckboxList } from '@woocommerce/blocks-components';
import Label from '@woocommerce/base-components/filter-element-label';
import FormTokenField from '@woocommerce/base-components/form-token-field';
import type { BlockEditProps } from '@wordpress/blocks';
import { getSetting } from '@woocommerce/settings';
import {
useCollectionData,
useQueryStateByContext,
} from '@woocommerce/base-context/hooks';
import { useCollectionData } from '@woocommerce/base-context/hooks';
/**
* Internal dependencies
@ -36,7 +32,7 @@ type StockStatusCount = {
count: number;
};
const Edit = ( props: BlockEditProps< BlockProps > & { context: Context } ) => {
const Edit = ( props: BlockEditProps< BlockProps > ) => {
const blockProps = useBlockProps( {
className: classnames(
'wc-block-stock-filter',
@ -50,11 +46,9 @@ const Edit = ( props: BlockEditProps< BlockProps > & { context: Context } ) => {
{}
);
const [ queryState ] = useQueryStateByContext();
const { results: filteredCounts } = useCollectionData( {
queryStock: true,
queryState,
queryState: {},
isEditor: true,
} );

View File

@ -1,71 +0,0 @@
/**
* External dependencies
*/
import { DEFAULT_QUERY } from '@woocommerce/blocks/product-collection/constants';
/**
* Internal dependencies
*/
import { sharedParams, mappedParams, formatQuery } from '../utils';
describe( 'formatQuery: transform Product Collection Block query to Product Collection Data Store API query', () => {
it( 'shared param is carried over', () => {
const formattedQuery = formatQuery( DEFAULT_QUERY );
sharedParams.forEach( ( key ) => {
expect( formattedQuery ).toHaveProperty(
key,
DEFAULT_QUERY[ key ]
);
} );
} );
it( 'mapped param key is transformed', () => {
const formattedQuery = formatQuery( DEFAULT_QUERY );
mappedParams.forEach( ( { key, map } ) => {
expect( formattedQuery ).toHaveProperty(
map,
DEFAULT_QUERY[ key ]
);
} );
} );
it( 'taxQuery is transformed', () => {
const queryWithTax = Object.assign( {}, DEFAULT_QUERY, {
taxQuery: {
product_cat: [ 1, 2 ],
product_tag: [ 3, 4 ],
custom_taxonomy: [ 5, 6 ],
},
} );
const formattedQuery = formatQuery( queryWithTax );
expect( formattedQuery ).toHaveProperty( 'cat', [ 1, 2 ] );
expect( formattedQuery ).toHaveProperty( 'tag', [ 3, 4 ] );
expect( formattedQuery ).toHaveProperty(
'_unstable_tax_custom_taxonomy',
[ 5, 6 ]
);
} );
it( 'attribute query is transformed', () => {
const woocommerceAttributes = [
{ termId: 11, taxonomy: 'pa_size' },
{ termId: 12, taxonomy: 'pa_color' },
{ termId: 13, taxonomy: 'pa_custom' },
{ termId: 14, taxonomy: 'pa_custom' },
];
const queryWithAttributes = Object.assign( {}, DEFAULT_QUERY, {
woocommerceAttributes,
} );
const formattedQuery = formatQuery( queryWithAttributes );
expect( formattedQuery ).toHaveProperty( 'attributes' );
woocommerceAttributes.forEach( ( { termId, taxonomy } ) => {
expect( formattedQuery.attributes ).toEqual(
expect.arrayContaining( [
{ term_id: termId, attribute: taxonomy },
] )
);
} );
} );
} );

View File

@ -1,15 +0,0 @@
/**
* External dependencies
*/
import type { BlockEditProps } from '@wordpress/blocks';
import type { ProductCollectionQuery } from '@woocommerce/blocks/product-collection/types';
type BlockAttributes = {
collectionData: unknown[];
};
export interface EditProps extends BlockEditProps< BlockAttributes > {
context: {
query: ProductCollectionQuery;
};
}

View File

@ -1,116 +0,0 @@
/**
* External dependencies
*/
import { objectHasProp } from '@woocommerce/types';
import type { BlockInstance } from '@wordpress/blocks';
import type { ProductCollectionQuery } from '@woocommerce/blocks/product-collection/types';
function mergeAttributeParams(
acc: Record< string, unknown >,
innerBlock: BlockInstance
) {
const current =
( acc?.calculate_attribute_counts as Array< unknown > ) ?? [];
acc.calculate_attribute_counts = [
...current,
innerBlock.attributes.queryParam.calculate_attribute_counts,
];
return acc;
}
function getInnerBlocksParams(
block: BlockInstance,
initial: Record< string, unknown > = {}
) {
return block.innerBlocks.reduce(
( acc, innerBlock ): Record< string, unknown > => {
if (
objectHasProp(
innerBlock.attributes.queryParam,
'calculate_attribute_counts'
)
) {
acc = mergeAttributeParams( acc, innerBlock );
} else {
acc = { ...acc, ...innerBlock.attributes?.queryParam };
}
return getInnerBlocksParams( innerBlock, acc );
},
initial
);
}
export function getQueryParams( block: BlockInstance | null ) {
if ( ! block ) return {};
return getInnerBlocksParams( block );
}
export const sharedParams: Array< keyof ProductCollectionQuery > = [
'exclude',
'offset',
'search',
];
/**
* There is an open dicussion around the shape of this object. Check it out on GH.
*
* @see {@link https://github.com/woocommerce/woocommerce-blocks/pull/11218#discussion_r1365171167 | #11218 review comment}.
*/
export const mappedParams: {
key: keyof ProductCollectionQuery;
map: string;
}[] = [
{ key: 'woocommerceStockStatus', map: 'stock_status' },
{ key: 'woocommerceOnSale', map: 'on_sale' },
{ key: 'woocommerceHandPickedProducts', map: 'include' },
];
function mapTaxonomy( taxonomy: string ) {
const map = {
product_tag: 'tag',
product_cat: 'cat',
};
return map[ taxonomy as keyof typeof map ] || `_unstable_tax_${ taxonomy }`;
}
function getTaxQueryMap( taxQuery: ProductCollectionQuery[ 'taxQuery' ] ) {
return Object.entries( taxQuery ).map( ( [ taxonomy, terms ] ) => ( {
[ mapTaxonomy( taxonomy ) ]: terms,
} ) );
}
function getAttributeQuery(
woocommerceAttributes: ProductCollectionQuery[ 'woocommerceAttributes' ]
) {
if ( ! woocommerceAttributes ) {
return {};
}
return woocommerceAttributes.map( ( attribute ) => ( {
attribute: attribute.taxonomy,
term_id: attribute.termId,
} ) );
}
export function formatQuery( query: ProductCollectionQuery ) {
if ( ! query ) {
return {};
}
return Object.assign(
{
attributes: getAttributeQuery( query.woocommerceAttributes ),
catalog_visibility: 'visible',
},
...sharedParams.map(
( key ) => key in query && { [ key ]: query[ key ] }
),
...mappedParams.map(
( param ) =>
param.key in query && { [ param.map ]: query[ param.key ] }
),
...getTaxQueryMap( query.taxQuery )
);
}

View File

@ -128,7 +128,7 @@ final class CollectionFilters extends AbstractBlock {
$response = Package::container()->get( Hydration::class )->get_rest_api_response_data(
add_query_arg(
array_merge(
$this->get_formatted_products_params( $block->context['query'] ),
$this->get_formatted_products_params( $block->context['query'] ?? array() ),
$collection_data_params,
),
'/wc/store/v1/products/collection-data'