From 09efe3f8b6771f9a34a1b2689c446086057bf926 Mon Sep 17 00:00:00 2001 From: tjcafferkey Date: Wed, 6 Apr 2022 15:29:51 +0100 Subject: [PATCH 1/5] Enable Attribute Filter block to work with the PHP rendered Classic Template block --- assets/js/blocks/attribute-filter/block.js | 102 +++++++++++++++++++++ assets/js/blocks/price-filter/block.js | 19 +--- assets/js/utils/filters.ts | 16 ++++ 3 files changed, 122 insertions(+), 15 deletions(-) create mode 100644 assets/js/utils/filters.ts diff --git a/assets/js/blocks/attribute-filter/block.js b/assets/js/blocks/attribute-filter/block.js index 4d087d2335c..6271b13ef50 100644 --- a/assets/js/blocks/attribute-filter/block.js +++ b/assets/js/blocks/attribute-filter/block.js @@ -19,16 +19,50 @@ import isShallowEqual from '@wordpress/is-shallow-equal'; import { decodeEntities } from '@wordpress/html-entities'; import { Notice } from '@wordpress/components'; import classNames from 'classnames'; +import { getSetting } from '@woocommerce/settings'; +import { addQueryArgs, removeQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ +import { getUrlParameter } from '../../utils/filters'; import { getAttributeFromID } from '../../utils/attributes'; import { updateAttributeFilter } from '../../utils/attributes-query'; import { previewAttributeObject, previewOptions } from './preview'; import { useBorderProps } from '../../hooks/style-attributes'; import './style.scss'; +/** + * Formats filter values into a string for the URL parameters needed for filtering PHP templates. + * + * @param {string} url Current page URL. + * @param {Array} params Parameters and their constraints. + * + * @return {string} New URL with query parameters in it. + */ +function formatParams( url, params = [] ) { + const paramObject = {}; + + params.forEach( ( param ) => { + const { attribute, operator, slug } = param; + + // Custom filters are prefix with `pa_` so we need to remove this. + const name = attribute.replace( 'pa_', '' ); + const values = slug.join( ',' ); + const queryType = `query_type_${ name }`; + const type = operator === 'or' ? 'in' : 'and'; + + // The URL parameter requires the prefix filter_ with the attribute name. + paramObject[ `filter_${ name }` ] = values; + paramObject[ queryType ] = type; + } ); + + // Clean the URL before we add our new query parameters to it. + const cleanUrl = removeQueryArgs( url, ...Object.keys( paramObject ) ); + + return addQueryArgs( cleanUrl, paramObject ); +} + /** * Component displaying an attribute filter. * @@ -40,6 +74,14 @@ const AttributeFilterBlock = ( { attributes: blockAttributes, isEditor = false, } ) => { + const filteringForPhpTemplate = getSetting( + 'is_rendering_php_template', + '' + ); + const [ hasSetPhpFilterDefaults, setHasSetPhpFilterDefaults ] = useState( + false + ); + const attributeObject = blockAttributes.isPreview && ! blockAttributes.attributeId ? previewAttributeObject @@ -332,6 +374,66 @@ const AttributeFilterBlock = ( { ] ); + /** + * Important: For PHP rendered block templates only. + * + * When we render the PHP block template (e.g. Classic Block) we need to set the default checked values, + * and also update the URL when the filters are clicked/updated. + */ + useEffect( () => { + if ( filteringForPhpTemplate && attributeObject ) { + const defaultAttributeParam = getUrlParameter( + `filter_${ attributeObject.name }` + ); + const defaultCheckedValue = + typeof defaultAttributeParam === 'string' + ? defaultAttributeParam.split( ',' ) + : []; + + if ( defaultCheckedValue.length > 0 && checked.length === 0 ) { + setChecked( defaultCheckedValue ); + } + + const newUrl = formatParams( + window.location.href, + productAttributesQuery + ); + if ( window.location.href !== newUrl ) { + window.location.href = newUrl; + } + } + }, [ + filteringForPhpTemplate, + productAttributesQuery, + checked.length, + attributeObject, + ] ); + + /** + * Important: For PHP rendered block templates only. + * + * When we set the default parameter values which we get from the URL in the above useEffect(), + * we need to run onSubmit which will set these values in state for the Active Filters block. + */ + useEffect( () => { + if ( filteringForPhpTemplate ) { + if ( + checked.length > 0 && + ! hasSetPhpFilterDefaults && + ! attributeTermsLoading + ) { + setHasSetPhpFilterDefaults( true ); + onSubmit( checked ); + } + } + }, [ + onSubmit, + filteringForPhpTemplate, + checked, + hasSetPhpFilterDefaults, + attributeTermsLoading, + ] ); + // Short-circuit if no attribute is selected. if ( ! attributeObject ) { if ( isEditor ) { diff --git a/assets/js/blocks/price-filter/block.js b/assets/js/blocks/price-filter/block.js index 79118100efe..4b647cb9b47 100644 --- a/assets/js/blocks/price-filter/block.js +++ b/assets/js/blocks/price-filter/block.js @@ -13,26 +13,15 @@ import { useDebouncedCallback } from 'use-debounce'; import PropTypes from 'prop-types'; import { getCurrencyFromPriceResponse } from '@woocommerce/price-format'; import { getSetting } from '@woocommerce/settings'; -import { getQueryArg, addQueryArgs, removeQueryArgs } from '@wordpress/url'; +import { addQueryArgs, removeQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ import usePriceConstraints from './use-price-constraints.js'; +import { getUrlParameter } from '../../utils/filters'; import './style.scss'; -/** - * Returns specified parameter from URL - * - * @param {string} paramName Parameter you want the value of. - */ -function findGetParameter( paramName ) { - if ( ! window ) { - return null; - } - return getQueryArg( window.location.href, paramName ); -} - /** * Formats filter values into a string for the URL parameters needed for filtering PHP templates. * @@ -71,8 +60,8 @@ const PriceFilterBlock = ( { attributes, isEditor = false } ) => { '' ); - const minPriceParam = findGetParameter( 'min_price' ); - const maxPriceParam = findGetParameter( 'max_price' ); + const minPriceParam = getUrlParameter( 'min_price' ); + const maxPriceParam = getUrlParameter( 'max_price' ); const [ minPriceQuery, setMinPriceQuery ] = useQueryStateByKey( 'min_price', diff --git a/assets/js/utils/filters.ts b/assets/js/utils/filters.ts new file mode 100644 index 00000000000..ba947cc0a8d --- /dev/null +++ b/assets/js/utils/filters.ts @@ -0,0 +1,16 @@ +/** + * External dependencies + */ +import { getQueryArg } from '@wordpress/url'; + +/** + * Returns specified parameter from URL + * + * @param {string} name Parameter you want the value of. + */ +export function getUrlParameter( name: string ) { + if ( ! window ) { + return null; + } + return getQueryArg( window.location.href, name ); +} From 859df13c8e3baa6e3eaf0ea406893856f65e5c95 Mon Sep 17 00:00:00 2001 From: tjcafferkey Date: Thu, 7 Apr 2022 14:19:26 +0100 Subject: [PATCH 2/5] Check for presence of option before rendering it --- .../js/base/components/dropdown-selector/index.js | 6 ++++++ assets/js/blocks/attribute-filter/block.js | 13 +++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/assets/js/base/components/dropdown-selector/index.js b/assets/js/base/components/dropdown-selector/index.js index 68573963819..2bc800c3140 100644 --- a/assets/js/base/components/dropdown-selector/index.js +++ b/assets/js/base/components/dropdown-selector/index.js @@ -127,10 +127,16 @@ const DropdownSelector = ( { const option = options.find( ( o ) => o.value === value ); + + if ( ! option ) { + return null; + } + const onRemoveItem = ( val ) => { onChange( val ); inputRef.current.focus(); }; + return multiple ? ( 0 && checked.length === 0 ) { + // When the component mounts let's set the active filters in state. + if ( + defaultCheckedValue.length > 0 && + checked.length === 0 && + ! hasSetPhpFilterDefaults + ) { setChecked( defaultCheckedValue ); } @@ -407,6 +412,7 @@ const AttributeFilterBlock = ( { productAttributesQuery, checked.length, attributeObject, + hasSetPhpFilterDefaults, ] ); /** @@ -423,7 +429,9 @@ const AttributeFilterBlock = ( { ! attributeTermsLoading ) { setHasSetPhpFilterDefaults( true ); - onSubmit( checked ); + if ( ! blockAttributes.showFilterButton ) { + onSubmit( checked ); + } } } }, [ @@ -432,6 +440,7 @@ const AttributeFilterBlock = ( { checked, hasSetPhpFilterDefaults, attributeTermsLoading, + blockAttributes.showFilterButton, ] ); // Short-circuit if no attribute is selected. From b468320c5cbe90bc4e57ed680cd1cf3005979b01 Mon Sep 17 00:00:00 2001 From: Luigi Date: Mon, 11 Apr 2022 15:56:14 +0200 Subject: [PATCH 3/5] improve filter product by attribute --- assets/js/blocks/attribute-filter/block.js | 80 ++++++++++------------ assets/js/blocks/attribute-filter/utils.ts | 69 +++++++++++++++++++ 2 files changed, 106 insertions(+), 43 deletions(-) create mode 100644 assets/js/blocks/attribute-filter/utils.ts diff --git a/assets/js/blocks/attribute-filter/block.js b/assets/js/blocks/attribute-filter/block.js index b66d7293d88..c922b796d78 100644 --- a/assets/js/blocks/attribute-filter/block.js +++ b/assets/js/blocks/attribute-filter/block.js @@ -19,18 +19,19 @@ import isShallowEqual from '@wordpress/is-shallow-equal'; import { decodeEntities } from '@wordpress/html-entities'; import { Notice } from '@wordpress/components'; import classNames from 'classnames'; -import { getSetting } from '@woocommerce/settings'; -import { addQueryArgs, removeQueryArgs } from '@wordpress/url'; +import { getSettingWithCoercion } from '@woocommerce/settings'; +import { getQueryArgs, removeQueryArgs } from '@wordpress/url'; +import { isBoolean } from '@woocommerce/types'; /** * Internal dependencies */ -import { getUrlParameter } from '../../utils/filters'; import { getAttributeFromID } from '../../utils/attributes'; import { updateAttributeFilter } from '../../utils/attributes-query'; import { previewAttributeObject, previewOptions } from './preview'; import { useBorderProps } from '../../hooks/style-attributes'; import './style.scss'; +import { formatParams, getActiveFilters, areAllFiltersRemoved } from './utils'; /** * Formats filter values into a string for the URL parameters needed for filtering PHP templates. @@ -40,28 +41,6 @@ import './style.scss'; * * @return {string} New URL with query parameters in it. */ -function formatParams( url, params = [] ) { - const paramObject = {}; - - params.forEach( ( param ) => { - const { attribute, operator, slug } = param; - - // Custom filters are prefix with `pa_` so we need to remove this. - const name = attribute.replace( 'pa_', '' ); - const values = slug.join( ',' ); - const queryType = `query_type_${ name }`; - const type = operator === 'or' ? 'in' : 'and'; - - // The URL parameter requires the prefix filter_ with the attribute name. - paramObject[ `filter_${ name }` ] = values; - paramObject[ queryType ] = type; - } ); - - // Clean the URL before we add our new query parameters to it. - const cleanUrl = removeQueryArgs( url, ...Object.keys( paramObject ) ); - - return addQueryArgs( cleanUrl, paramObject ); -} /** * Component displaying an attribute filter. @@ -74,10 +53,12 @@ const AttributeFilterBlock = ( { attributes: blockAttributes, isEditor = false, } ) => { - const filteringForPhpTemplate = getSetting( + const filteringForPhpTemplate = getSettingWithCoercion( 'is_rendering_php_template', - '' + false, + isBoolean ); + const [ hasSetPhpFilterDefaults, setHasSetPhpFilterDefaults ] = useState( false ); @@ -87,7 +68,10 @@ const AttributeFilterBlock = ( { ? previewAttributeObject : getAttributeFromID( blockAttributes.attributeId ); - const [ checked, setChecked ] = useState( [] ); + const [ checked, setChecked ] = useState( + getActiveFilters( filteringForPhpTemplate, attributeObject ) + ); + const [ displayedOptions, setDisplayedOptions ] = useState( blockAttributes.isPreview && ! blockAttributes.attributeId ? previewOptions @@ -125,6 +109,10 @@ const AttributeFilterBlock = ( { }, queryState: { ...queryState, + // The PHP template renders only the products with the visibility set to catalog + ...( filteringForPhpTemplate && { + catalog_visibility: 'catalog', + } ), attributes: filterAvailableTerms ? queryState.attributes : null, }, } ); @@ -382,26 +370,31 @@ const AttributeFilterBlock = ( { */ useEffect( () => { if ( filteringForPhpTemplate && attributeObject ) { - const defaultAttributeParam = getUrlParameter( - `filter_${ attributeObject.name }` - ); - const defaultCheckedValue = - typeof defaultAttributeParam === 'string' - ? defaultAttributeParam.split( ',' ) - : []; - - // When the component mounts let's set the active filters in state. if ( - defaultCheckedValue.length > 0 && - checked.length === 0 && - ! hasSetPhpFilterDefaults + areAllFiltersRemoved( { + currentCheckedFilters: checked, + hasSetPhpFilterDefaults, + } ) ) { - setChecked( defaultCheckedValue ); + setChecked( [] ); + const currentQueryArgs = Object.keys( + getQueryArgs( window.location.href ) + ); + + const url = currentQueryArgs.reduce( + ( currentUrl, queryArg ) => + removeQueryArgs( currentUrl, queryArg ), + window.location.href + ); + + window.location.href = url; } + setChecked( checked ); const newUrl = formatParams( window.location.href, - productAttributesQuery + productAttributesQuery, + blockAttributes.queryType ); if ( window.location.href !== newUrl ) { window.location.href = newUrl; @@ -410,9 +403,10 @@ const AttributeFilterBlock = ( { }, [ filteringForPhpTemplate, productAttributesQuery, - checked.length, attributeObject, hasSetPhpFilterDefaults, + checked, + blockAttributes.queryType, ] ); /** diff --git a/assets/js/blocks/attribute-filter/utils.ts b/assets/js/blocks/attribute-filter/utils.ts new file mode 100644 index 00000000000..f5b557553a7 --- /dev/null +++ b/assets/js/blocks/attribute-filter/utils.ts @@ -0,0 +1,69 @@ +/** + * External dependencies + */ +import { addQueryArgs, removeQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import { getUrlParameter } from '../../utils/filters'; + +interface Param { + attribute: string; + operator: string; + slug: Array< string >; +} + +export const formatParams = ( + url: string, + params: Array< Param > = [], + blockOperator: 'or' | 'and' +) => { + const paramObject: Record< string, string > = {}; + + params.forEach( ( param ) => { + const { attribute, slug } = param; + + // Custom filters are prefix with `pa_` so we need to remove this. + const name = attribute.replace( 'pa_', '' ); + const values = slug.join( ',' ); + const queryType = `query_type_${ name }`; + const type = blockOperator === 'or' ? 'or' : 'and'; + + // The URL parameter requires the prefix filter_ with the attribute name. + paramObject[ `filter_${ name }` ] = values; + paramObject[ queryType ] = type; + } ); + + // Clean the URL before we add our new query parameters to it. + const cleanUrl = removeQueryArgs( url, ...Object.keys( paramObject ) ); + + return addQueryArgs( cleanUrl, paramObject ); +}; + +export const areAllFiltersRemoved = ( { + currentCheckedFilters, + hasSetPhpFilterDefaults, +}: { + currentCheckedFilters: Array< string >; + hasSetPhpFilterDefaults: boolean; +} ) => hasSetPhpFilterDefaults && currentCheckedFilters.length === 0; + +export const getActiveFilters = ( + isFilteringForPhpTemplateEnabled: boolean, + attributeObject: Record< string, string > | undefined +) => { + if ( isFilteringForPhpTemplateEnabled && attributeObject ) { + const defaultAttributeParam = getUrlParameter( + `filter_${ attributeObject.name }` + ); + const defaultCheckedValue = + typeof defaultAttributeParam === 'string' + ? defaultAttributeParam.split( ',' ) + : []; + + return defaultCheckedValue; + } + + return []; +}; From 89a173df7a43d7267295ea6f756e5aee5092fbd7 Mon Sep 17 00:00:00 2001 From: Luigi Date: Mon, 11 Apr 2022 17:14:55 +0200 Subject: [PATCH 4/5] fix pagination problem --- assets/js/blocks/attribute-filter/block.js | 14 +++++++++++--- src/BlockTypes/ClassicTemplate.php | 7 +++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/assets/js/blocks/attribute-filter/block.js b/assets/js/blocks/attribute-filter/block.js index c922b796d78..506cfac3fa2 100644 --- a/assets/js/blocks/attribute-filter/block.js +++ b/assets/js/blocks/attribute-filter/block.js @@ -21,7 +21,7 @@ import { Notice } from '@wordpress/components'; import classNames from 'classnames'; import { getSettingWithCoercion } from '@woocommerce/settings'; import { getQueryArgs, removeQueryArgs } from '@wordpress/url'; -import { isBoolean } from '@woocommerce/types'; +import { isBoolean, isString } from '@woocommerce/types'; /** * Internal dependencies @@ -59,6 +59,12 @@ const AttributeFilterBlock = ( { isBoolean ); + const pageUrl = getSettingWithCoercion( + 'page_url', + window.location.href, + isString + ); + const [ hasSetPhpFilterDefaults, setHasSetPhpFilterDefaults ] = useState( false ); @@ -392,11 +398,12 @@ const AttributeFilterBlock = ( { setChecked( checked ); const newUrl = formatParams( - window.location.href, + pageUrl, productAttributesQuery, blockAttributes.queryType ); - if ( window.location.href !== newUrl ) { + + if ( pageUrl !== newUrl ) { window.location.href = newUrl; } } @@ -407,6 +414,7 @@ const AttributeFilterBlock = ( { hasSetPhpFilterDefaults, checked, blockAttributes.queryType, + pageUrl, ] ); /** diff --git a/src/BlockTypes/ClassicTemplate.php b/src/BlockTypes/ClassicTemplate.php index a1e451ea41e..2ba5d6e41b6 100644 --- a/src/BlockTypes/ClassicTemplate.php +++ b/src/BlockTypes/ClassicTemplate.php @@ -66,6 +66,13 @@ protected function render( $attributes, $content ) { true, null ); + + $this->asset_data_registry->add( + 'page_url', + html_entity_decode( get_pagenum_link() ), + '' + ); + return $this->render_archive_product(); } else { ob_start(); From 0bdf8a7eae6d79dbee4c030d60eb3cc8c06f1496 Mon Sep 17 00:00:00 2001 From: Luigi Date: Tue, 12 Apr 2022 17:56:01 +0200 Subject: [PATCH 5/5] fix check when two filter block with same attribute are used --- assets/js/blocks/attribute-filter/block.js | 26 +++++++++------- assets/js/blocks/attribute-filter/utils.ts | 36 +++++++++++++++++----- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/assets/js/blocks/attribute-filter/block.js b/assets/js/blocks/attribute-filter/block.js index 506cfac3fa2..9c392fd45c0 100644 --- a/assets/js/blocks/attribute-filter/block.js +++ b/assets/js/blocks/attribute-filter/block.js @@ -31,7 +31,12 @@ import { updateAttributeFilter } from '../../utils/attributes-query'; import { previewAttributeObject, previewOptions } from './preview'; import { useBorderProps } from '../../hooks/style-attributes'; import './style.scss'; -import { formatParams, getActiveFilters, areAllFiltersRemoved } from './utils'; +import { + formatParams, + getActiveFilters, + areAllFiltersRemoved, + isQueryArgsEqual, +} from './utils'; /** * Formats filter values into a string for the URL parameters needed for filtering PHP templates. @@ -383,27 +388,26 @@ const AttributeFilterBlock = ( { } ) ) { setChecked( [] ); - const currentQueryArgs = Object.keys( + const currentQueryArgKeys = Object.keys( getQueryArgs( window.location.href ) ); - const url = currentQueryArgs.reduce( + const url = currentQueryArgKeys.reduce( ( currentUrl, queryArg ) => removeQueryArgs( currentUrl, queryArg ), window.location.href ); - window.location.href = url; + const newUrl = formatParams( url, productAttributesQuery ); + window.location.href = newUrl; } setChecked( checked ); - const newUrl = formatParams( - pageUrl, - productAttributesQuery, - blockAttributes.queryType - ); + const newUrl = formatParams( pageUrl, productAttributesQuery ); + const currentQueryArgs = getQueryArgs( window.location.href ); + const newUrlQueryArgs = getQueryArgs( newUrl ); - if ( pageUrl !== newUrl ) { + if ( ! isQueryArgsEqual( currentQueryArgs, newUrlQueryArgs ) ) { window.location.href = newUrl; } } @@ -411,10 +415,10 @@ const AttributeFilterBlock = ( { filteringForPhpTemplate, productAttributesQuery, attributeObject, - hasSetPhpFilterDefaults, checked, blockAttributes.queryType, pageUrl, + hasSetPhpFilterDefaults, ] ); /** diff --git a/assets/js/blocks/attribute-filter/utils.ts b/assets/js/blocks/attribute-filter/utils.ts index f5b557553a7..b5017be629b 100644 --- a/assets/js/blocks/attribute-filter/utils.ts +++ b/assets/js/blocks/attribute-filter/utils.ts @@ -2,6 +2,7 @@ * External dependencies */ import { addQueryArgs, removeQueryArgs } from '@wordpress/url'; +import { QueryArgs } from '@wordpress/url/build-types/get-query-args'; /** * Internal dependencies @@ -14,21 +15,17 @@ interface Param { slug: Array< string >; } -export const formatParams = ( - url: string, - params: Array< Param > = [], - blockOperator: 'or' | 'and' -) => { +export const formatParams = ( url: string, params: Array< Param > = [] ) => { const paramObject: Record< string, string > = {}; params.forEach( ( param ) => { - const { attribute, slug } = param; + const { attribute, slug, operator } = param; // Custom filters are prefix with `pa_` so we need to remove this. const name = attribute.replace( 'pa_', '' ); const values = slug.join( ',' ); const queryType = `query_type_${ name }`; - const type = blockOperator === 'or' ? 'or' : 'and'; + const type = operator === 'in' ? 'or' : 'and'; // The URL parameter requires the prefix filter_ with the attribute name. paramObject[ `filter_${ name }` ] = values; @@ -67,3 +64,28 @@ export const getActiveFilters = ( return []; }; + +export const isQueryArgsEqual = ( + currentQueryArgs: QueryArgs, + newQueryArgs: QueryArgs +) => { + // The user can add same two filter blocks for the same attribute. + // We removed the query type from the check to avoid refresh loop. + const filteredNewQueryArgs = Object.entries( newQueryArgs ).reduce( + ( acc, [ key, value ] ) => { + return key.includes( 'query_type' ) + ? acc + : { + ...acc, + [ key ]: value, + }; + }, + {} + ); + + return Object.entries( filteredNewQueryArgs ).reduce( + ( isEqual, [ key, value ] ) => + currentQueryArgs[ key ] === value ? isEqual : false, + true + ); +};