Active filters block (https://github.com/woocommerce/woocommerce-blocks/pull/1168)
* Basic block construction * Register on PHP side * wc-active-filters script * Price utils * Refactor price slider so state reflects the query Moves some logic from the component to the block so that min and max price can change (via query) and be reflected by the price sliders. This allows the active filters block to change the query and have those new values reflected by the slider. * Fix type checking of numbers * Styles for filter block * Improved attribute helper for getting attribute taxonomy data from ID/taxonomy * Refactor attribute filter to use updateAttributeFilter helper * Disable checkboxes when loading to avoid multiple queries * Add todos - this is blocked * Remove checked state from Attribute Filter so it gets updated from the store (https://github.com/woocommerce/woocommerce-blocks/pull/1170) * isLoading check * active price filtering rendering * Block heading * Implement block options; chip display with clear button * Clear all should remove all attributes * Enable previews * Introduce a component to look up terms from slugs using collections (which are cached) * Correct all docblocks * activePriceFilters null return * renderRemovableListItem * Remove useMemo for hasFilters * Switch classnames notation * Ensure slug is array in removeAttributeFilterBySlug * null -> undefined return types for attributes * Remove fragment * Check we have a termObject in ActiveAttributeFilters * Refactor formatPriceRange return statements * Ensure query array index will exist * Only sort when adding a query * Remove aria-label with dupe text * hasFilters is function * Update useQueryStateByKey usage * More doc block fixes * Update getAttributeFromTaxonomy return and docblock * getAttributeFromID return/docblock
This commit is contained in:
parent
0739e4c536
commit
d613d2fde6
|
@ -32,7 +32,7 @@ import { COLLECTIONS_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
|||
* @type {bool} addingToCart Whether the product is currently being
|
||||
* added to the cart (true).
|
||||
* @type {bool} cartIsLoading Whether the cart is being loaded.
|
||||
* @type {function} addToCart An action dispatcher for adding a single
|
||||
* @type {Function} addToCart An action dispatcher for adding a single
|
||||
* quantity of the product to the cart.
|
||||
* Receives no arguments, it operates on the
|
||||
* current product.
|
||||
|
|
|
@ -3,13 +3,7 @@
|
|||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Fragment,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
useEffect,
|
||||
} from '@wordpress/element';
|
||||
import { Fragment, useMemo, useState } from '@wordpress/element';
|
||||
import classNames from 'classnames';
|
||||
|
||||
/**
|
||||
|
@ -24,17 +18,13 @@ const CheckboxList = ( {
|
|||
className,
|
||||
onChange = () => {},
|
||||
options = [],
|
||||
checked = [],
|
||||
isLoading = false,
|
||||
isDisabled = false,
|
||||
limit = 10,
|
||||
} ) => {
|
||||
// Holds all checked options.
|
||||
const [ checked, setChecked ] = useState( [] );
|
||||
const [ showExpanded, setShowExpanded ] = useState( false );
|
||||
|
||||
useEffect( () => {
|
||||
onChange( checked );
|
||||
}, [ checked ] );
|
||||
|
||||
const placeholder = useMemo( () => {
|
||||
return [ ...Array( 5 ) ].map( ( x, i ) => (
|
||||
<li
|
||||
|
@ -47,24 +37,6 @@ const CheckboxList = ( {
|
|||
) );
|
||||
}, [] );
|
||||
|
||||
const onCheckboxChange = useCallback(
|
||||
( event ) => {
|
||||
const isChecked = event.target.checked;
|
||||
const checkedValue = event.target.value;
|
||||
const newChecked = checked.filter(
|
||||
( value ) => value !== checkedValue
|
||||
);
|
||||
|
||||
if ( isChecked ) {
|
||||
newChecked.push( checkedValue );
|
||||
newChecked.sort();
|
||||
}
|
||||
|
||||
setChecked( newChecked );
|
||||
},
|
||||
[ checked ]
|
||||
);
|
||||
|
||||
const renderedShowMore = useMemo( () => {
|
||||
const optionCount = options.length;
|
||||
return (
|
||||
|
@ -135,8 +107,9 @@ const CheckboxList = ( {
|
|||
type="checkbox"
|
||||
id={ option.key }
|
||||
value={ option.key }
|
||||
onChange={ onCheckboxChange }
|
||||
onChange={ onChange }
|
||||
checked={ checked.includes( option.key ) }
|
||||
disabled={ isDisabled }
|
||||
/>
|
||||
<label htmlFor={ option.key }>
|
||||
{ option.label }
|
||||
|
@ -155,9 +128,9 @@ const CheckboxList = ( {
|
|||
checked,
|
||||
showExpanded,
|
||||
limit,
|
||||
onCheckboxChange,
|
||||
renderedShowLess,
|
||||
renderedShowMore,
|
||||
isDisabled,
|
||||
] );
|
||||
|
||||
const classes = classNames(
|
||||
|
@ -183,8 +156,10 @@ CheckboxList.propTypes = {
|
|||
label: PropTypes.node.isRequired,
|
||||
} )
|
||||
),
|
||||
checked: PropTypes.array,
|
||||
className: PropTypes.string,
|
||||
isLoading: PropTypes.bool,
|
||||
isDisabled: PropTypes.bool,
|
||||
limit: PropTypes.number,
|
||||
};
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* @param {integer} pagesToDisplay Maximum number of pages to display in the pagination component.
|
||||
* @param {integer} currentPage Page currently visible.
|
||||
* @param {integer} totalPages Total pages available.
|
||||
* @return {object} Object containing the min and max index to display in the pagination component.
|
||||
* @return {Object} Object containing the min and max index to display in the pagination component.
|
||||
*/
|
||||
export const getIndexes = ( pagesToDisplay, currentPage, totalPages ) => {
|
||||
if ( totalPages <= 2 ) {
|
||||
|
|
|
@ -12,94 +12,60 @@ import {
|
|||
} from '@wordpress/element';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { useDebounce, usePrevious } from '@woocommerce/base-hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import { constrainRangeSliderValues, formatCurrencyForInput } from './utils';
|
||||
import { constrainRangeSliderValues } from './utils';
|
||||
import { formatPrice } from '../../utils/price';
|
||||
import SubmitButton from './submit-button';
|
||||
import PriceLabel from './price-label';
|
||||
import PriceInput from './price-input';
|
||||
|
||||
const PriceSlider = ( {
|
||||
initialMin,
|
||||
initialMax,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
minConstraint,
|
||||
maxConstraint,
|
||||
onChange,
|
||||
step,
|
||||
currencySymbol,
|
||||
priceFormat,
|
||||
showInputFields,
|
||||
showFilterButton,
|
||||
isLoading,
|
||||
onChange = () => {},
|
||||
step = 10,
|
||||
currencySymbol = '$',
|
||||
priceFormat = '%1$s%2$s',
|
||||
showInputFields = true,
|
||||
showFilterButton = false,
|
||||
isLoading = false,
|
||||
onSubmit = () => {},
|
||||
} ) => {
|
||||
const minRange = useRef();
|
||||
const maxRange = useRef();
|
||||
const [ minPrice, setMinPrice ] = useState( initialMin );
|
||||
const [ maxPrice, setMaxPrice ] = useState( initialMax );
|
||||
|
||||
const [ formattedMinPrice, setFormattedMinPrice ] = useState(
|
||||
formatCurrencyForInput( minPrice, priceFormat, currencySymbol )
|
||||
formatPrice( minPrice, priceFormat, currencySymbol )
|
||||
);
|
||||
const [ formattedMaxPrice, setFormattedMaxPrice ] = useState(
|
||||
formatCurrencyForInput( maxPrice, priceFormat, currencySymbol )
|
||||
formatPrice( maxPrice, priceFormat, currencySymbol )
|
||||
);
|
||||
const debouncedChangeValue = useDebounce( [ minPrice, maxPrice ], 500 );
|
||||
const prevMinConstraint = usePrevious( minConstraint );
|
||||
const prevMaxConstraint = usePrevious( maxConstraint );
|
||||
|
||||
useEffect( () => {
|
||||
if ( isNaN( minConstraint ) ) {
|
||||
setMinPrice( 0 );
|
||||
return;
|
||||
}
|
||||
if (
|
||||
minPrice === undefined ||
|
||||
minConstraint > minPrice ||
|
||||
minPrice === prevMinConstraint
|
||||
) {
|
||||
setMinPrice( minConstraint );
|
||||
}
|
||||
}, [ minConstraint ] );
|
||||
|
||||
useEffect( () => {
|
||||
if ( isNaN( maxConstraint ) ) {
|
||||
setMaxPrice( 100 );
|
||||
return;
|
||||
}
|
||||
if (
|
||||
maxPrice === undefined ||
|
||||
maxConstraint < maxPrice ||
|
||||
maxPrice === prevMaxConstraint
|
||||
) {
|
||||
setMaxPrice( maxConstraint );
|
||||
}
|
||||
}, [ maxConstraint ] );
|
||||
|
||||
useEffect( () => {
|
||||
setFormattedMinPrice(
|
||||
formatCurrencyForInput( minPrice, priceFormat, currencySymbol )
|
||||
formatPrice( minPrice, priceFormat, currencySymbol )
|
||||
);
|
||||
}, [ minPrice, priceFormat, currencySymbol ] );
|
||||
|
||||
useEffect( () => {
|
||||
setFormattedMaxPrice(
|
||||
formatCurrencyForInput( maxPrice, priceFormat, currencySymbol )
|
||||
formatPrice( maxPrice, priceFormat, currencySymbol )
|
||||
);
|
||||
}, [ maxPrice, priceFormat, currencySymbol ] );
|
||||
|
||||
/**
|
||||
* Checks if the min and max constraints are valid.
|
||||
*/
|
||||
const hasValidConstraints = useMemo( () => {
|
||||
return isFinite( minConstraint ) && isFinite( maxConstraint );
|
||||
}, [ minConstraint, maxConstraint ] );
|
||||
|
||||
useEffect( () => {
|
||||
if ( ! showFilterButton && ! isLoading && hasValidConstraints ) {
|
||||
triggerChange();
|
||||
}
|
||||
}, [ debouncedChangeValue ] );
|
||||
|
||||
/**
|
||||
* Handles styles for the shaded area of the range slider.
|
||||
*/
|
||||
|
@ -140,13 +106,6 @@ const PriceSlider = ( {
|
|||
hasValidConstraints,
|
||||
] );
|
||||
|
||||
/**
|
||||
* Trigger the onChange prop callback with new values.
|
||||
*/
|
||||
const triggerChange = useCallback( () => {
|
||||
onChange( [ minPrice, maxPrice ] );
|
||||
}, [ minPrice, maxPrice ] );
|
||||
|
||||
/**
|
||||
* Works around an IE issue where only one range selector is visible by changing the display order
|
||||
* based on the mouse position.
|
||||
|
@ -206,8 +165,10 @@ const PriceSlider = ( {
|
|||
step,
|
||||
isMin
|
||||
);
|
||||
setMinPrice( parseInt( values[ 0 ], 10 ) );
|
||||
setMaxPrice( parseInt( values[ 1 ], 10 ) );
|
||||
onChange( [
|
||||
parseInt( values[ 0 ], 10 ),
|
||||
parseInt( values[ 1 ], 10 ),
|
||||
] );
|
||||
},
|
||||
[ minPrice, maxPrice, minConstraint, maxConstraint, step ]
|
||||
);
|
||||
|
@ -232,8 +193,24 @@ const PriceSlider = ( {
|
|||
step,
|
||||
isMin
|
||||
);
|
||||
setMinPrice( parseInt( values[ 0 ], 10 ) );
|
||||
setMaxPrice( parseInt( values[ 1 ], 10 ) );
|
||||
onChange( [
|
||||
parseInt( values[ 0 ], 10 ),
|
||||
parseInt( values[ 1 ], 10 ),
|
||||
] );
|
||||
setFormattedMinPrice(
|
||||
formatPrice(
|
||||
parseInt( values[ 0 ], 10 ),
|
||||
priceFormat,
|
||||
currencySymbol
|
||||
)
|
||||
);
|
||||
setFormattedMaxPrice(
|
||||
formatPrice(
|
||||
parseInt( values[ 1 ], 10 ),
|
||||
priceFormat,
|
||||
currencySymbol
|
||||
)
|
||||
);
|
||||
},
|
||||
[ minPrice, maxPrice, minConstraint, maxConstraint, step ]
|
||||
);
|
||||
|
@ -250,19 +227,11 @@ const PriceSlider = ( {
|
|||
);
|
||||
if ( isMin ) {
|
||||
setFormattedMinPrice(
|
||||
formatCurrencyForInput(
|
||||
newValue,
|
||||
priceFormat,
|
||||
currencySymbol
|
||||
)
|
||||
formatPrice( newValue, priceFormat, currencySymbol )
|
||||
);
|
||||
} else {
|
||||
setFormattedMaxPrice(
|
||||
formatCurrencyForInput(
|
||||
newValue,
|
||||
priceFormat,
|
||||
currencySymbol
|
||||
)
|
||||
formatPrice( newValue, priceFormat, currencySymbol )
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -339,7 +308,7 @@ const PriceSlider = ( {
|
|||
{ showFilterButton && (
|
||||
<SubmitButton
|
||||
disabled={ isLoading || ! hasValidConstraints }
|
||||
onClick={ triggerChange }
|
||||
onClick={ onSubmit }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
|
@ -353,13 +322,17 @@ PriceSlider.propTypes = {
|
|||
*/
|
||||
onChange: PropTypes.func.isRequired,
|
||||
/**
|
||||
* Initial min value.
|
||||
* Callback fired when the filter button is pressed.
|
||||
*/
|
||||
initialMin: PropTypes.number,
|
||||
onSubmit: PropTypes.func,
|
||||
/**
|
||||
* Initial max value.
|
||||
* Min value.
|
||||
*/
|
||||
initialMax: PropTypes.number,
|
||||
minPrice: PropTypes.number,
|
||||
/**
|
||||
* Max value.
|
||||
*/
|
||||
maxPrice: PropTypes.number,
|
||||
/**
|
||||
* Minimum allowed price.
|
||||
*/
|
||||
|
@ -394,13 +367,4 @@ PriceSlider.propTypes = {
|
|||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
PriceSlider.defaultProps = {
|
||||
step: 1,
|
||||
currencySymbol: '$',
|
||||
priceFormat: '%1$s%2$s',
|
||||
showInputFields: true,
|
||||
showFilterButton: false,
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
export default PriceSlider;
|
||||
|
|
|
@ -1,17 +1,12 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { sprintf } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Validate a min and max value for a range slider againt defined constraints (min, max, step).
|
||||
*
|
||||
* @param {array} values Array containing min and max values.
|
||||
* @param {Array} values Array containing min and max values.
|
||||
* @param {int} min Min allowed value for the sliders.
|
||||
* @param {int} max Max allowed value for the sliders.
|
||||
* @param {step} step Step value for the sliders.
|
||||
* @param {boolean} isMin Whether we're currently interacting with the min range slider or not, so we update the correct values.
|
||||
* @returns {array} Validated and updated min/max values that fit within the range slider constraints.
|
||||
* @returns {Array} Validated and updated min/max values that fit within the range slider constraints.
|
||||
*/
|
||||
export const constrainRangeSliderValues = ( values, min, max, step, isMin ) => {
|
||||
let minValue = parseInt( values[ 0 ], 10 ) || min;
|
||||
|
@ -43,31 +38,3 @@ export const constrainRangeSliderValues = ( values, min, max, step, isMin ) => {
|
|||
|
||||
return [ minValue, maxValue ];
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a price with currency data.
|
||||
*
|
||||
* @param {number} value Number to format.
|
||||
* @param {string} priceFormat Price format string.
|
||||
* @param {string} currencySymbol Curency symbol.
|
||||
*/
|
||||
export const formatCurrencyForInput = (
|
||||
value,
|
||||
priceFormat,
|
||||
currencySymbol
|
||||
) => {
|
||||
if ( value === '' || undefined === value ) {
|
||||
return '';
|
||||
}
|
||||
const formattedNumber = parseInt( value, 10 );
|
||||
const formattedValue = sprintf(
|
||||
priceFormat,
|
||||
currencySymbol,
|
||||
formattedNumber
|
||||
);
|
||||
|
||||
// This uses a textarea to magically decode HTML currency symbols.
|
||||
const txt = document.createElement( 'textarea' );
|
||||
txt.innerHTML = formattedValue;
|
||||
return txt.value;
|
||||
};
|
||||
|
|
|
@ -7,8 +7,8 @@ import { getBlockMap } from '../../../blocks/products/base-utils';
|
|||
* Maps a layout config into atomic components.
|
||||
*
|
||||
* @param {string} blockName Name of the parent block. Used to get extension children.
|
||||
* @param {object} product Product object to pass to atomic components.
|
||||
* @param {object[]} layoutConfig Object with component data.
|
||||
* @param {Object} product Product object to pass to atomic components.
|
||||
* @param {Object[]} layoutConfig Object with component data.
|
||||
* @param {number} componentId Parent component ID needed for key generation.
|
||||
*/
|
||||
export const renderProductLayout = (
|
||||
|
|
|
@ -24,7 +24,7 @@ export const truncateHtml = ( html, length, ellipsis = '...' ) => {
|
|||
* value of the lines prop. Content is updated once limited.
|
||||
*
|
||||
* @param {string} originalContent Content to be clamped.
|
||||
* @param {object} targetElement Element which will contain the clamped content.
|
||||
* @param {Object} targetElement Element which will contain the clamped content.
|
||||
* @param {integer} maxHeight Max height of the clamped content.
|
||||
* @param {string} ellipsis Character to append to clamped content.
|
||||
* @return {string} clamped content
|
||||
|
@ -44,7 +44,7 @@ export const clampLines = (
|
|||
* Calculate how long the content can be based on the maximum number of lines allowed, and client height.
|
||||
*
|
||||
* @param {string} originalContent Content to be clamped.
|
||||
* @param {object} targetElement Element which will contain the clamped content.
|
||||
* @param {Object} targetElement Element which will contain the clamped content.
|
||||
* @param {integer} maxHeight Max height of the clamped content.
|
||||
*/
|
||||
const calculateLength = ( originalContent, targetElement, maxHeight ) => {
|
||||
|
@ -72,7 +72,7 @@ const calculateLength = ( originalContent, targetElement, maxHeight ) => {
|
|||
/**
|
||||
* Move string markers. Used by calculateLength.
|
||||
*
|
||||
* @param {object} markers Markers for clamped content.
|
||||
* @param {Object} markers Markers for clamped content.
|
||||
* @param {integer} currentHeight Current height of clamped content.
|
||||
* @param {integer} maxHeight Max height of the clamped content.
|
||||
*/
|
||||
|
|
|
@ -3,5 +3,4 @@ export * from './use-shallow-equal';
|
|||
export * from './use-store-products';
|
||||
export * from './use-collection';
|
||||
export * from './use-collection-header';
|
||||
export * from './use-debounce';
|
||||
export * from './use-previous';
|
||||
|
|
|
@ -31,7 +31,7 @@ describe( 'Testing Query State Hooks', () => {
|
|||
*
|
||||
* @param {Object} testRenderer An instance of the created test component.
|
||||
*
|
||||
* @return {array} A tuple containing the expected query value as the first
|
||||
* @return {Array} A tuple containing the expected query value as the first
|
||||
* element and the expected query action creator as the
|
||||
* second argument.
|
||||
*/
|
||||
|
@ -61,9 +61,9 @@ describe( 'Testing Query State Hooks', () => {
|
|||
* expected PropKeys for obtaining the values to be fed to the hook as
|
||||
* arguments.
|
||||
*
|
||||
* @param {function} hookTested The hook being tested to use in the
|
||||
* @param {Function} hookTested The hook being tested to use in the
|
||||
* test comopnent.
|
||||
* @param {array} propKeysForArgs An array of keys for the props that
|
||||
* @param {Array} propKeysForArgs An array of keys for the props that
|
||||
* will be used on the test component that
|
||||
* will have values fed to the tested
|
||||
* hook.
|
||||
|
|
|
@ -25,7 +25,7 @@ import { useShallowEqual } from './use-shallow-equal';
|
|||
* @param {string} options.resourceName The name of the resource for the
|
||||
* collection. Example:
|
||||
* `'products/attributes'`
|
||||
* @param {array} options.resourceValues An array of values (in correct order)
|
||||
* @param {Array} options.resourceValues An array of values (in correct order)
|
||||
* that are substituted in the route
|
||||
* placeholders for the collection route.
|
||||
* Example: `[10, 20]`
|
||||
|
|
|
@ -21,7 +21,7 @@ import { useShallowEqual } from './use-shallow-equal';
|
|||
* @param {string} options.resourceName The name of the resource for the
|
||||
* collection. Example:
|
||||
* `'products/attributes'`
|
||||
* @param {array} options.resourceValues An array of values (in correct order)
|
||||
* @param {Array} options.resourceValues An array of values (in correct order)
|
||||
* that are substituted in the route
|
||||
* placeholders for the collection route.
|
||||
* Example: `[10, 20]`
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Debounce effects based on https://dev.to/gabe_ragland/debouncing-with-react-hooks-jci
|
||||
* @param {mixed} value
|
||||
* @param {number} delay
|
||||
*/
|
||||
export const useDebounce = ( value, delay = 500 ) => {
|
||||
// State and setters for debounced value
|
||||
const [ debouncedValue, setDebouncedValue ] = useState( value );
|
||||
|
||||
useEffect( () => {
|
||||
// Set debouncedValue to value (passed in) after the specified delay
|
||||
const handler = setTimeout( () => {
|
||||
setDebouncedValue( value );
|
||||
}, delay );
|
||||
return () => {
|
||||
clearTimeout( handler );
|
||||
};
|
||||
}, [ value ] );
|
||||
|
||||
return debouncedValue;
|
||||
};
|
|
@ -23,7 +23,7 @@ import { useShallowEqual } from './use-shallow-equal';
|
|||
* from the query state context provided by the
|
||||
* QueryStateContextProvider
|
||||
*
|
||||
* @return {array} An array that has two elements. The first element is the
|
||||
* @return {Array} An array that has two elements. The first element is the
|
||||
* query state value for the given context. The second element
|
||||
* is a dispatcher function for setting the query state.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export * from './errors';
|
||||
export * from './price';
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { sprintf } from '@wordpress/i18n';
|
||||
import { CURRENCY } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Format a price with currency data.
|
||||
*
|
||||
* @param {number} value Number to format.
|
||||
* @param {string} priceFormat Price format string.
|
||||
* @param {string} currencySymbol Curency symbol.
|
||||
*/
|
||||
export const formatPrice = (
|
||||
value,
|
||||
priceFormat = CURRENCY.price_format,
|
||||
currencySymbol = CURRENCY.symbol
|
||||
) => {
|
||||
if ( value === '' || undefined === value ) {
|
||||
return '';
|
||||
}
|
||||
const formattedNumber = parseInt( value, 10 );
|
||||
const formattedValue = sprintf(
|
||||
priceFormat,
|
||||
currencySymbol,
|
||||
formattedNumber
|
||||
);
|
||||
|
||||
// This uses a textarea to magically decode HTML currency symbols.
|
||||
const txt = document.createElement( 'textarea' );
|
||||
txt.innerHTML = formattedValue;
|
||||
return txt.value;
|
||||
};
|
|
@ -26,7 +26,7 @@ const assertOption = ( options, optionName, expectedType ) => {
|
|||
* @param {Object} options Options to use when registering the block.
|
||||
* @param {string} options.main Name of the parent block.
|
||||
* @param {string} options.blockName Name of the child block being registered.
|
||||
* @param {function} options.component React component used to render the child block.
|
||||
* @param {Function} options.component React component used to render the child block.
|
||||
*/
|
||||
export function registerInnerBlock( options ) {
|
||||
assertOption( options, 'main', 'string' );
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useCollection, useQueryStateByKey } from '@woocommerce/base-hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { renderRemovableListItem } from './utils';
|
||||
import { removeAttributeFilterBySlug } from '../../utils/attributes-query';
|
||||
|
||||
/**
|
||||
* Component that renders active attribute (terms) filters.
|
||||
*/
|
||||
const ActiveAttributeFilters = ( { attributeObject = {}, slugs = [] } ) => {
|
||||
const { results, isLoading } = useCollection( {
|
||||
namespace: '/wc/store',
|
||||
resourceName: 'products/attributes/terms',
|
||||
resourceValues: [ attributeObject.id ],
|
||||
} );
|
||||
|
||||
const [ productAttributes, setProductAttributes ] = useQueryStateByKey(
|
||||
'attributes',
|
||||
[]
|
||||
);
|
||||
|
||||
if ( isLoading ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const attributeLabel = attributeObject.label;
|
||||
|
||||
return slugs.map( ( slug ) => {
|
||||
const termObject = results.find( ( term ) => {
|
||||
return term.slug === slug;
|
||||
} );
|
||||
|
||||
return (
|
||||
termObject &&
|
||||
renderRemovableListItem(
|
||||
attributeLabel,
|
||||
termObject.name || slug,
|
||||
() => {
|
||||
removeAttributeFilterBySlug(
|
||||
productAttributes,
|
||||
setProductAttributes,
|
||||
attributeObject,
|
||||
slug
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
} );
|
||||
};
|
||||
|
||||
export default ActiveAttributeFilters;
|
|
@ -0,0 +1,118 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useQueryStateByKey } from '@woocommerce/base-hooks';
|
||||
import { useMemo, Fragment } from '@wordpress/element';
|
||||
import classnames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import { getAttributeFromTaxonomy } from '../../utils/attributes';
|
||||
import { formatPriceRange, renderRemovableListItem } from './utils';
|
||||
import ActiveAttributeFilters from './active-attribute-filters';
|
||||
|
||||
/**
|
||||
* Component displaying active filters.
|
||||
*/
|
||||
const ActiveFiltersBlock = ( {
|
||||
attributes: blockAttributes,
|
||||
isPreview = false,
|
||||
} ) => {
|
||||
const [ productAttributes, setProductAttributes ] = useQueryStateByKey(
|
||||
'attributes',
|
||||
[]
|
||||
);
|
||||
const [ minPrice, setMinPrice ] = useQueryStateByKey( 'min_price' );
|
||||
const [ maxPrice, setMaxPrice ] = useQueryStateByKey( 'max_price' );
|
||||
|
||||
const activePriceFilters = useMemo( () => {
|
||||
if ( ! Number.isFinite( minPrice ) && ! Number.isFinite( maxPrice ) ) {
|
||||
return null;
|
||||
}
|
||||
return renderRemovableListItem(
|
||||
__( 'Price:', 'woo-gutenberg-products-block' ),
|
||||
formatPriceRange( minPrice, maxPrice ),
|
||||
() => {
|
||||
setMinPrice( null );
|
||||
setMaxPrice( null );
|
||||
}
|
||||
);
|
||||
}, [ minPrice, maxPrice, formatPriceRange ] );
|
||||
|
||||
const activeAttributeFilters = useMemo( () => {
|
||||
return productAttributes.map( ( attribute ) => {
|
||||
const attributeObject = getAttributeFromTaxonomy(
|
||||
attribute.attribute
|
||||
);
|
||||
return (
|
||||
<ActiveAttributeFilters
|
||||
attributeObject={ attributeObject }
|
||||
slugs={ attribute.slug }
|
||||
key={ attribute.attribute }
|
||||
/>
|
||||
);
|
||||
} );
|
||||
}, [ productAttributes ] );
|
||||
|
||||
const hasFilters = () => {
|
||||
return (
|
||||
productAttributes.length > 0 ||
|
||||
Number.isFinite( minPrice ) ||
|
||||
Number.isFinite( maxPrice )
|
||||
);
|
||||
};
|
||||
|
||||
if ( ! hasFilters() && ! isPreview ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const TagName = `h${ blockAttributes.headingLevel }`;
|
||||
const listClasses = classnames( 'wc-block-active-filters-list', {
|
||||
'wc-block-active-filters-list--chips':
|
||||
blockAttributes.displayStyle === 'chips',
|
||||
} );
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{ ! isPreview && blockAttributes.heading && (
|
||||
<TagName>{ blockAttributes.heading }</TagName>
|
||||
) }
|
||||
<div className="wc-block-active-filters">
|
||||
<ul className={ listClasses }>
|
||||
{ isPreview ? (
|
||||
<Fragment>
|
||||
{ renderRemovableListItem(
|
||||
__( 'Size', 'woo-gutenberg-products-block' ),
|
||||
__( 'Small', 'woo-gutenberg-products-block' )
|
||||
) }
|
||||
{ renderRemovableListItem(
|
||||
__( 'Color', 'woo-gutenberg-products-block' ),
|
||||
__( 'Blue', 'woo-gutenberg-products-block' )
|
||||
) }
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
{ activePriceFilters }
|
||||
{ activeAttributeFilters }
|
||||
</Fragment>
|
||||
) }
|
||||
</ul>
|
||||
<button
|
||||
className="wc-block-active-filters__clear-all"
|
||||
onClick={ () => {
|
||||
setMinPrice( null );
|
||||
setMaxPrice( null );
|
||||
setProductAttributes( [] );
|
||||
} }
|
||||
>
|
||||
{ __( 'Clear All', 'woo-gutenberg-products-block' ) }
|
||||
</button>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActiveFiltersBlock;
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Fragment } from '@wordpress/element';
|
||||
import { InspectorControls, PlainText } from '@wordpress/editor';
|
||||
import { Disabled, PanelBody, withSpokenMessages } from '@wordpress/components';
|
||||
import HeadingToolbar from '@woocommerce/block-components/heading-toolbar';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block.js';
|
||||
import ToggleButtonControl from '../../components/toggle-button-control';
|
||||
|
||||
const Edit = ( { attributes, setAttributes } ) => {
|
||||
const getInspectorControls = () => {
|
||||
const { displayStyle } = attributes;
|
||||
|
||||
return (
|
||||
<InspectorControls key="inspector">
|
||||
<PanelBody
|
||||
title={ __(
|
||||
'Block Settings',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
>
|
||||
<ToggleButtonControl
|
||||
label={ __(
|
||||
'Display Style',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
value={ displayStyle }
|
||||
options={ [
|
||||
{
|
||||
label: __(
|
||||
'List',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
value: 'list',
|
||||
},
|
||||
{
|
||||
label: __(
|
||||
'Chips',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
value: 'chips',
|
||||
},
|
||||
] }
|
||||
onChange={ ( value ) =>
|
||||
setAttributes( {
|
||||
displayStyle: value,
|
||||
} )
|
||||
}
|
||||
/>
|
||||
<p>
|
||||
{ __(
|
||||
'Heading Level',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</p>
|
||||
<HeadingToolbar
|
||||
isCollapsed={ false }
|
||||
minLevel={ 2 }
|
||||
maxLevel={ 7 }
|
||||
selectedLevel={ attributes.headingLevel }
|
||||
onChange={ ( newLevel ) =>
|
||||
setAttributes( { headingLevel: newLevel } )
|
||||
}
|
||||
/>
|
||||
</PanelBody>
|
||||
</InspectorControls>
|
||||
);
|
||||
};
|
||||
|
||||
const TagName = `h${ attributes.headingLevel }`;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{ getInspectorControls() }
|
||||
<TagName>
|
||||
<PlainText
|
||||
className="wc-block-attribute-filter-heading"
|
||||
value={ attributes.heading }
|
||||
onChange={ ( value ) =>
|
||||
setAttributes( { heading: value } )
|
||||
}
|
||||
/>
|
||||
</TagName>
|
||||
<Disabled>
|
||||
<Block attributes={ attributes } isPreview />
|
||||
</Disabled>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default withSpokenMessages( Edit );
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block.js';
|
||||
import renderFrontend from '../../utils/render-frontend.js';
|
||||
|
||||
const getProps = ( el ) => {
|
||||
return {
|
||||
attributes: {
|
||||
displayStyle: el.dataset.displayStyle,
|
||||
heading: el.dataset.heading,
|
||||
headingLevel: el.dataset.headingLevel || 3,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
renderFrontend( '.wp-block-woocommerce-active-filters', Block, getProps );
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
import Gridicon from 'gridicons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import edit from './edit.js';
|
||||
|
||||
registerBlockType( 'woocommerce/active-filters', {
|
||||
title: __( 'Active Product Filters', 'woo-gutenberg-products-block' ),
|
||||
icon: {
|
||||
src: <Gridicon icon="list-checkmark" />,
|
||||
foreground: '#96588a',
|
||||
},
|
||||
category: 'woocommerce',
|
||||
keywords: [ __( 'WooCommerce', 'woo-gutenberg-products-block' ) ],
|
||||
description: __(
|
||||
'Display a list of active product filters.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
supports: {},
|
||||
example: {
|
||||
attributes: {},
|
||||
},
|
||||
attributes: {
|
||||
displayStyle: {
|
||||
type: 'string',
|
||||
default: 'list',
|
||||
},
|
||||
heading: {
|
||||
type: 'string',
|
||||
default: __( 'Active filters', 'woo-gutenberg-products-block' ),
|
||||
},
|
||||
headingLevel: {
|
||||
type: 'number',
|
||||
default: 3,
|
||||
},
|
||||
},
|
||||
edit,
|
||||
/**
|
||||
* Save the props to post content.
|
||||
*/
|
||||
save( { attributes } ) {
|
||||
const { displayStyle, heading, headingLevel } = attributes;
|
||||
const data = {
|
||||
'data-display-style': displayStyle,
|
||||
'data-heading': heading,
|
||||
'data-heading-level': headingLevel,
|
||||
};
|
||||
return (
|
||||
<div className="is-loading" { ...data }>
|
||||
<span
|
||||
aria-hidden
|
||||
className="wc-block-active-product-filters__placeholder"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
} );
|
|
@ -0,0 +1,90 @@
|
|||
.wc-block-active-filters {
|
||||
margin: 0 0 $gap;
|
||||
overflow: hidden;
|
||||
|
||||
.wc-block-active-filters__clear-all {
|
||||
float: right;
|
||||
background: transparent none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
|
||||
&:hover {
|
||||
background: transparent none;
|
||||
}
|
||||
}
|
||||
.wc-block-active-filters-list {
|
||||
margin: 0 0 $gap-smallest;
|
||||
list-style: none outside;
|
||||
clear: both;
|
||||
|
||||
li {
|
||||
margin: 0 0 $gap-smallest;
|
||||
padding: 0 16px 0 0;
|
||||
list-style: none outside;
|
||||
clear: both;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
button {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
appearance: none;
|
||||
height: 0;
|
||||
padding: 16px 0 0 0;
|
||||
width: 16px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
margin: -8px 0 0 0;
|
||||
|
||||
&::before {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: transparent url("data:image/svg+xml,%3Csvg viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='9' cy='9' r='9' fill='%2324292d'/%3E%3Crect x='4.5' y='6.8866' width='3.375' height='9.9466' transform='rotate(-45 4.5 6.8866)' fill='white'/%3E%3Crect x='11.5334' y='4.5' width='3.375' height='9.9466' transform='rotate(45 11.5334 4.5)' fill='white'/%3E%3C/svg%3E%0A") center center no-repeat; /* stylelint-disable-line */
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.wc-block-active-filters-list--chips {
|
||||
li {
|
||||
display: inline-block;
|
||||
background: #c4c4c4;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
margin: 0 6px 6px 0;
|
||||
color: #24292d;
|
||||
|
||||
.wc-block-active-filters-list-item__type {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
button {
|
||||
float: none;
|
||||
vertical-align: middle;
|
||||
margin: -2px 0 0 9px;
|
||||
height: 0;
|
||||
padding: 12px 0 0 0;
|
||||
width: 12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: transparent url("data:image/svg+xml,%3Csvg width='12' viewBox='0 0 9 9' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='7.03329' width='2' height='9.9466' transform='rotate(45 7.03329 0)' fill='%2324292d'/%3E%3Crect x='8.4476' y='7.07104' width='2' height='9.9466' transform='rotate(135 8.4476 7.07104)' fill='%2324292d'/%3E%3C/svg%3E%0A") center center no-repeat; /* stylelint-disable-line */
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { formatPrice } from '@woocommerce/base-utils';
|
||||
|
||||
/**
|
||||
* Format a min/max price range to display.
|
||||
* @param {number} minPrice The min price, if set.
|
||||
* @param {number} maxPrice The max price, if set.
|
||||
*/
|
||||
export const formatPriceRange = ( minPrice, maxPrice ) => {
|
||||
if ( Number.isFinite( minPrice ) && Number.isFinite( maxPrice ) ) {
|
||||
/* translators: %s min price, %s max price */
|
||||
return sprintf(
|
||||
__( 'Between %s and %s', 'woo-gutenberg-products-block' ),
|
||||
formatPrice( minPrice ),
|
||||
formatPrice( maxPrice )
|
||||
);
|
||||
}
|
||||
|
||||
if ( Number.isFinite( minPrice ) ) {
|
||||
/* translators: %s min price */
|
||||
return sprintf(
|
||||
__( 'From %s', 'woo-gutenberg-products-block' ),
|
||||
formatPrice( minPrice )
|
||||
);
|
||||
}
|
||||
|
||||
/* translators: %s max price */
|
||||
return sprintf(
|
||||
__( 'Up to %s', 'woo-gutenberg-products-block' ),
|
||||
formatPrice( maxPrice )
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Render a removable item in the active filters block list.
|
||||
* @param {string} type Type string.
|
||||
* @param {string} name Name string.
|
||||
* @param {Function} removeCallback Callback to remove item.
|
||||
*/
|
||||
export const renderRemovableListItem = (
|
||||
type,
|
||||
name,
|
||||
removeCallback = () => {}
|
||||
) => {
|
||||
return (
|
||||
<li
|
||||
className="wc-block-active-filters-list-item"
|
||||
key={ type + ':' + name }
|
||||
>
|
||||
<span className="wc-block-active-filters-list-item__type">
|
||||
{ type + ': ' }
|
||||
</span>
|
||||
<strong className="wc-block-active-filters-list-item__name">
|
||||
{ name }
|
||||
</strong>
|
||||
<button onClick={ removeCallback }>
|
||||
{ __( 'Remove', 'woo-gutenberg-products-block' ) }
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
};
|
|
@ -13,7 +13,6 @@ import {
|
|||
useState,
|
||||
useMemo,
|
||||
} from '@wordpress/element';
|
||||
import { sortBy } from 'lodash';
|
||||
import CheckboxList from '@woocommerce/base-components/checkbox-list';
|
||||
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
|
||||
|
||||
|
@ -21,30 +20,41 @@ import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundar
|
|||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import { getTaxonomyFromAttributeId } from '../../utils/attributes';
|
||||
import { getAttributeFromID } from '../../utils/attributes';
|
||||
import { updateAttributeFilter } from '../../utils/attributes-query';
|
||||
|
||||
/**
|
||||
* Component displaying an attribute filter.
|
||||
*/
|
||||
const AttributeFilterBlock = ( { attributes, isPreview = false } ) => {
|
||||
const [ options, setOptions ] = useState( [] );
|
||||
const [ checkedOptions, setCheckedOptions ] = useState( [] );
|
||||
const { showCounts, attributeId, queryType } = attributes;
|
||||
const taxonomy = getTaxonomyFromAttributeId( attributeId );
|
||||
const AttributeFilterBlock = ( {
|
||||
attributes: blockAttributes,
|
||||
isPreview = false,
|
||||
} ) => {
|
||||
const [ displayedOptions, setDisplayedOptions ] = useState( [] );
|
||||
const attributeObject = getAttributeFromID( blockAttributes.attributeId );
|
||||
const [ queryState ] = useQueryStateByContext();
|
||||
const [ productAttributes, setProductAttributes ] = useQueryStateByKey(
|
||||
'attributes',
|
||||
[]
|
||||
);
|
||||
const [
|
||||
productAttributesQuery,
|
||||
setProductAttributesQuery,
|
||||
] = useQueryStateByKey( 'attributes', [] );
|
||||
|
||||
const checked = useMemo( () => {
|
||||
return productAttributesQuery
|
||||
.filter(
|
||||
( attribute ) =>
|
||||
attribute.attribute === attributeObject.taxonomy
|
||||
)
|
||||
.flatMap( ( attribute ) => attribute.slug );
|
||||
}, [ productAttributesQuery, attributeObject ] );
|
||||
|
||||
const filteredCountsQueryState = useMemo( () => {
|
||||
// If doing an "AND" query, we need to remove current taxonomy query so counts are not affected.
|
||||
const modifiedQueryState =
|
||||
queryType === 'or'
|
||||
? productAttributes.filter(
|
||||
( item ) => item.attribute !== taxonomy
|
||||
blockAttributes.queryType === 'or'
|
||||
? productAttributesQuery.filter(
|
||||
( item ) => item.attribute !== attributeObject.taxonomy
|
||||
)
|
||||
: productAttributes;
|
||||
: productAttributesQuery;
|
||||
|
||||
// Take current query and remove paging args.
|
||||
return {
|
||||
|
@ -54,9 +64,14 @@ const AttributeFilterBlock = ( { attributes, isPreview = false } ) => {
|
|||
per_page: undefined,
|
||||
page: undefined,
|
||||
attributes: modifiedQueryState,
|
||||
calculate_attribute_counts: [ taxonomy ],
|
||||
calculate_attribute_counts: [ attributeObject.taxonomy ],
|
||||
};
|
||||
}, [ queryState, taxonomy, queryType, productAttributes ] );
|
||||
}, [
|
||||
queryState,
|
||||
attributeObject,
|
||||
blockAttributes,
|
||||
productAttributesQuery,
|
||||
] );
|
||||
|
||||
const {
|
||||
results: attributeTerms,
|
||||
|
@ -64,7 +79,7 @@ const AttributeFilterBlock = ( { attributes, isPreview = false } ) => {
|
|||
} = useCollection( {
|
||||
namespace: '/wc/store',
|
||||
resourceName: 'products/attributes/terms',
|
||||
resourceValues: [ attributeId ],
|
||||
resourceValues: [ attributeObject.id ],
|
||||
} );
|
||||
|
||||
const {
|
||||
|
@ -76,12 +91,15 @@ const AttributeFilterBlock = ( { attributes, isPreview = false } ) => {
|
|||
query: filteredCountsQueryState,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Get the label for an attribute term filter.
|
||||
*/
|
||||
const getLabel = useCallback(
|
||||
( name, count ) => {
|
||||
return (
|
||||
<Fragment key="label">
|
||||
{ name }
|
||||
{ showCounts && (
|
||||
{ blockAttributes.showCounts && count !== null && (
|
||||
<span className="wc-block-attribute-filter-list-count">
|
||||
{ count }
|
||||
</span>
|
||||
|
@ -89,13 +107,16 @@ const AttributeFilterBlock = ( { attributes, isPreview = false } ) => {
|
|||
</Fragment>
|
||||
);
|
||||
},
|
||||
[ showCounts ]
|
||||
[ blockAttributes ]
|
||||
);
|
||||
|
||||
/**
|
||||
* Get count data about a given term by ID.
|
||||
*/
|
||||
const getFilteredTerm = useCallback(
|
||||
( id ) => {
|
||||
if ( ! filteredCounts.attribute_counts ) {
|
||||
return {};
|
||||
return null;
|
||||
}
|
||||
return filteredCounts.attribute_counts.find(
|
||||
( { term } ) => term === id
|
||||
|
@ -108,7 +129,6 @@ const AttributeFilterBlock = ( { attributes, isPreview = false } ) => {
|
|||
* Compare intersection of all terms and filtered counts to get a list of options to display.
|
||||
*/
|
||||
useEffect( () => {
|
||||
// Do nothing until we have the attribute terms from the API.
|
||||
if ( attributeTermsLoading || filteredCountsLoading ) {
|
||||
return;
|
||||
}
|
||||
|
@ -117,73 +137,103 @@ const AttributeFilterBlock = ( { attributes, isPreview = false } ) => {
|
|||
|
||||
attributeTerms.forEach( ( term ) => {
|
||||
const filteredTerm = getFilteredTerm( term.id );
|
||||
const isChecked = checkedOptions.includes( term.slug );
|
||||
const inCollection = !! filteredTerm;
|
||||
const isChecked = checked.includes( term.slug );
|
||||
const count = filteredTerm ? filteredTerm.count : null;
|
||||
|
||||
// If there is no match this term doesn't match the current product collection - only render if checked.
|
||||
if ( ! inCollection && ! isChecked ) {
|
||||
if ( ! filteredTerm && ! isChecked ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredCount = filteredTerm
|
||||
? filteredTerm.count
|
||||
: term.count;
|
||||
const count = ! inCollection && isChecked ? 0 : filteredCount;
|
||||
|
||||
newOptions.push( {
|
||||
key: term.slug,
|
||||
label: getLabel( term.name, count ),
|
||||
} );
|
||||
} );
|
||||
|
||||
setOptions( newOptions );
|
||||
setDisplayedOptions( newOptions );
|
||||
}, [
|
||||
filteredCountsLoading,
|
||||
attributeTerms,
|
||||
attributeTermsLoading,
|
||||
filteredCountsLoading,
|
||||
getFilteredTerm,
|
||||
getLabel,
|
||||
checkedOptions,
|
||||
checked,
|
||||
] );
|
||||
|
||||
useEffect( () => {
|
||||
const newProductAttributes = productAttributes.filter(
|
||||
( item ) => item.attribute !== taxonomy
|
||||
);
|
||||
/**
|
||||
* Returns an array of term objects that have been chosen via the checkboxes.
|
||||
*/
|
||||
const getSelectedTerms = useCallback(
|
||||
( newChecked ) => {
|
||||
return attributeTerms.reduce( ( acc, term ) => {
|
||||
if ( newChecked.includes( term.slug ) ) {
|
||||
acc.push( term );
|
||||
}
|
||||
return acc;
|
||||
}, [] );
|
||||
},
|
||||
[ attributeTerms ]
|
||||
);
|
||||
|
||||
if ( checkedOptions ) {
|
||||
const updatedQuery = {
|
||||
attribute: taxonomy,
|
||||
operator: queryType === 'or' ? 'in' : 'and',
|
||||
slug: checkedOptions,
|
||||
};
|
||||
newProductAttributes.push( updatedQuery );
|
||||
}
|
||||
/**
|
||||
* When a checkbox in the list changes, update state.
|
||||
*/
|
||||
const onChange = useCallback(
|
||||
( event ) => {
|
||||
const isChecked = event.target.checked;
|
||||
const checkedValue = event.target.value;
|
||||
const newChecked = checked.filter(
|
||||
( value ) => value !== checkedValue
|
||||
);
|
||||
|
||||
setProductAttributes( sortBy( newProductAttributes, 'attribute' ) );
|
||||
}, [ checkedOptions, taxonomy, productAttributes, queryType ] );
|
||||
if ( isChecked ) {
|
||||
newChecked.push( checkedValue );
|
||||
newChecked.sort();
|
||||
}
|
||||
|
||||
const onChange = useCallback( ( checked ) => {
|
||||
setCheckedOptions( checked );
|
||||
}, [] );
|
||||
const newSelectedTerms = getSelectedTerms( newChecked );
|
||||
|
||||
if ( ! taxonomy || ( options.length === 0 && ! attributeTermsLoading ) ) {
|
||||
updateAttributeFilter(
|
||||
productAttributesQuery,
|
||||
setProductAttributesQuery,
|
||||
attributeObject,
|
||||
newSelectedTerms,
|
||||
blockAttributes.queryType === 'or' ? 'in' : 'and'
|
||||
);
|
||||
},
|
||||
[
|
||||
attributeTerms,
|
||||
checked,
|
||||
productAttributesQuery,
|
||||
setProductAttributesQuery,
|
||||
attributeObject,
|
||||
blockAttributes,
|
||||
]
|
||||
);
|
||||
|
||||
if (
|
||||
! attributeObject ||
|
||||
( displayedOptions.length === 0 && ! attributeTermsLoading )
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const TagName = `h${ attributes.headingLevel }`;
|
||||
const TagName = `h${ blockAttributes.headingLevel }`;
|
||||
|
||||
return (
|
||||
<BlockErrorBoundary>
|
||||
{ ! isPreview && attributes.heading && (
|
||||
<TagName>{ attributes.heading }</TagName>
|
||||
{ ! isPreview && blockAttributes.heading && (
|
||||
<TagName>{ blockAttributes.heading }</TagName>
|
||||
) }
|
||||
<div className="wc-block-attribute-filter">
|
||||
<CheckboxList
|
||||
className={ 'wc-block-attribute-filter-list' }
|
||||
options={ options }
|
||||
options={ displayedOptions }
|
||||
checked={ checked }
|
||||
onChange={ onChange }
|
||||
isLoading={ attributeTermsLoading }
|
||||
isDisabled={ filteredCountsLoading }
|
||||
/>
|
||||
</div>
|
||||
</BlockErrorBoundary>
|
||||
|
|
|
@ -55,7 +55,6 @@ registerBlockType( 'woocommerce/attribute-filter', {
|
|||
save( { attributes } ) {
|
||||
const {
|
||||
showCounts,
|
||||
displayStyle,
|
||||
queryType,
|
||||
attributeId,
|
||||
heading,
|
||||
|
@ -64,7 +63,6 @@ registerBlockType( 'woocommerce/attribute-filter', {
|
|||
const data = {
|
||||
'data-attribute-id': attributeId,
|
||||
'data-show-counts': showCounts,
|
||||
'data-display-style': displayStyle,
|
||||
'data-query-type': queryType,
|
||||
'data-heading': heading,
|
||||
'data-heading-level': headingLevel,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.wc-block-attribute-filter {
|
||||
.wc-block-attribute-filter-list {
|
||||
margin: 0 0 $gap-small;
|
||||
margin: 0 0 $gap;
|
||||
|
||||
li {
|
||||
text-decoration: underline;
|
||||
|
|
|
@ -5,18 +5,24 @@ import {
|
|||
useCollection,
|
||||
useQueryStateByKey,
|
||||
useQueryStateByContext,
|
||||
usePrevious,
|
||||
} from '@woocommerce/base-hooks';
|
||||
import { useCallback } from '@wordpress/element';
|
||||
import { useCallback, useState, useEffect } from '@wordpress/element';
|
||||
import PriceSlider from '@woocommerce/base-components/price-slider';
|
||||
import { CURRENCY } from '@woocommerce/settings';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
|
||||
|
||||
/**
|
||||
* Component displaying a price filter.
|
||||
*/
|
||||
const PriceFilterBlock = ( { attributes, isPreview = false } ) => {
|
||||
const [ minPrice, setMinPrice ] = useQueryStateByKey( 'min-price' );
|
||||
const [ maxPrice, setMaxPrice ] = useQueryStateByKey( 'max_price' );
|
||||
const [ minPriceQuery, setMinPriceQuery ] = useQueryStateByKey(
|
||||
'min_price'
|
||||
);
|
||||
const [ maxPriceQuery, setMaxPriceQuery ] = useQueryStateByKey(
|
||||
'max_price'
|
||||
);
|
||||
const [ queryState ] = useQueryStateByContext();
|
||||
const { results, isLoading } = useCollection( {
|
||||
namespace: '/wc/store',
|
||||
|
@ -33,33 +39,115 @@ const PriceFilterBlock = ( { attributes, isPreview = false } ) => {
|
|||
},
|
||||
} );
|
||||
|
||||
const { showInputFields, showFilterButton } = attributes;
|
||||
const minConstraint = isLoading
|
||||
? undefined
|
||||
: // Round up to nearest 10 to match the step attribute.
|
||||
Math.floor( parseInt( results.min_price, 10 ) / 10 ) * 10;
|
||||
const maxConstraint = isLoading
|
||||
? undefined
|
||||
: // Round down to nearest 10 to match the step attribute.
|
||||
Math.ceil( parseInt( results.max_price, 10 ) / 10 ) * 10;
|
||||
const [ minPrice, setMinPrice ] = useState();
|
||||
const [ maxPrice, setMaxPrice ] = useState();
|
||||
const [ minConstraint, setMinConstraint ] = useState();
|
||||
const [ maxConstraint, setMaxConstraint ] = useState();
|
||||
const prevMinConstraint = usePrevious( minConstraint );
|
||||
const prevMaxConstraint = usePrevious( maxConstraint );
|
||||
|
||||
// Updates the query after a short delay.
|
||||
const [ debouncedUpdateQuery ] = useDebouncedCallback( () => {
|
||||
onSubmit();
|
||||
}, 500 );
|
||||
|
||||
// Updates the query based on slider values.
|
||||
const onSubmit = useCallback( () => {
|
||||
setMinPriceQuery( minPrice === minConstraint ? undefined : minPrice );
|
||||
setMaxPriceQuery( maxPrice === maxConstraint ? undefined : maxPrice );
|
||||
}, [ minPrice, maxPrice, minConstraint, maxConstraint ] );
|
||||
|
||||
// Callback when slider is changed.
|
||||
const onChange = useCallback(
|
||||
( prices ) => {
|
||||
if ( prices[ 0 ] === minConstraint ) {
|
||||
setMinPrice( undefined );
|
||||
} else if ( prices[ 0 ] !== minPrice ) {
|
||||
if ( prices[ 0 ] !== minPrice ) {
|
||||
setMinPrice( prices[ 0 ] );
|
||||
}
|
||||
|
||||
if ( prices[ 1 ] === maxConstraint ) {
|
||||
setMaxPrice( undefined );
|
||||
} else if ( prices[ 1 ] !== maxPrice ) {
|
||||
if ( prices[ 1 ] !== maxPrice ) {
|
||||
setMaxPrice( prices[ 1 ] );
|
||||
}
|
||||
},
|
||||
[ minConstraint, maxConstraint, minPrice, maxPrice ]
|
||||
);
|
||||
|
||||
// Track price STATE changes - if state changes, update the query.
|
||||
useEffect( () => {
|
||||
debouncedUpdateQuery();
|
||||
}, [ minPrice, maxPrice ] );
|
||||
|
||||
// Track PRICE QUERY changes so the slider reflects current filters.
|
||||
useEffect( () => {
|
||||
if ( minPriceQuery !== minPrice ) {
|
||||
setMinPrice(
|
||||
Number.isFinite( minPriceQuery ) ? minPriceQuery : minConstraint
|
||||
);
|
||||
}
|
||||
if ( maxPriceQuery !== maxPrice ) {
|
||||
setMaxPrice(
|
||||
Number.isFinite( maxPriceQuery ) ? maxPriceQuery : maxConstraint
|
||||
);
|
||||
}
|
||||
}, [ minPriceQuery, maxPriceQuery, minConstraint, maxConstraint ] );
|
||||
|
||||
// Track product updates to update constraints.
|
||||
useEffect( () => {
|
||||
if ( isLoading ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const minPriceFromQuery = results.min_price;
|
||||
const maxPriceFromQuery = results.max_price;
|
||||
|
||||
if ( isNaN( minPriceFromQuery ) ) {
|
||||
setMinConstraint( null );
|
||||
} else {
|
||||
// Round up to nearest 10 to match the step attribute.
|
||||
setMinConstraint(
|
||||
Math.floor( parseInt( minPriceFromQuery, 10 ) / 10 ) * 10
|
||||
);
|
||||
}
|
||||
|
||||
if ( isNaN( maxPriceFromQuery ) ) {
|
||||
setMaxConstraint( null );
|
||||
} else {
|
||||
// Round down to nearest 10 to match the step attribute.
|
||||
setMaxConstraint(
|
||||
Math.ceil( parseInt( maxPriceFromQuery, 10 ) / 10 ) * 10
|
||||
);
|
||||
}
|
||||
}, [ isLoading, results ] );
|
||||
|
||||
// Track min constraint changes.
|
||||
useEffect( () => {
|
||||
if (
|
||||
minPrice === undefined ||
|
||||
minConstraint > minPrice ||
|
||||
minPrice === prevMinConstraint
|
||||
) {
|
||||
setMinPrice( minConstraint );
|
||||
}
|
||||
}, [ minConstraint ] );
|
||||
|
||||
// Track max constraint changes.
|
||||
useEffect( () => {
|
||||
if (
|
||||
maxPrice === undefined ||
|
||||
maxConstraint < maxPrice ||
|
||||
maxPrice === prevMaxConstraint
|
||||
) {
|
||||
setMaxPrice( maxConstraint );
|
||||
}
|
||||
}, [ maxConstraint ] );
|
||||
|
||||
if (
|
||||
! isLoading &&
|
||||
( minConstraint === null ||
|
||||
maxConstraint === null ||
|
||||
minConstraint === maxConstraint )
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const TagName = `h${ attributes.headingLevel }`;
|
||||
|
||||
return (
|
||||
|
@ -71,14 +159,15 @@ const PriceFilterBlock = ( { attributes, isPreview = false } ) => {
|
|||
<PriceSlider
|
||||
minConstraint={ minConstraint }
|
||||
maxConstraint={ maxConstraint }
|
||||
initialMin={ undefined }
|
||||
initialMax={ undefined }
|
||||
minPrice={ minPrice }
|
||||
maxPrice={ maxPrice }
|
||||
step={ 10 }
|
||||
currencySymbol={ CURRENCY.symbol }
|
||||
priceFormat={ CURRENCY.price_format }
|
||||
showInputFields={ showInputFields }
|
||||
showFilterButton={ showFilterButton }
|
||||
showInputFields={ attributes.showInputFields }
|
||||
showFilterButton={ attributes.showFilterButton }
|
||||
onChange={ onChange }
|
||||
onSubmit={ onSubmit }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -42,8 +42,8 @@ export const DEFAULT_PRODUCT_LIST_LAYOUT = [
|
|||
/**
|
||||
* Converts innerblocks to a list of layout configs.
|
||||
*
|
||||
* @param {object} blockMap Map of blocks as returned by `getBlockMap`.
|
||||
* @param {object[]} innerBlocks Inner block components.
|
||||
* @param {Object} blockMap Map of blocks as returned by `getBlockMap`.
|
||||
* @param {Object[]} innerBlocks Inner block components.
|
||||
*/
|
||||
export const getProductLayoutConfig = ( blockMap, innerBlocks ) => {
|
||||
if ( ! innerBlocks || innerBlocks.length === 0 ) {
|
||||
|
|
|
@ -23,7 +23,7 @@ Headers = Headers
|
|||
* @param {string} namespace The namespace for the collection route.
|
||||
* @param {string} resourceName The resource name for the collection route.
|
||||
* @param {string} [queryString=''] The query string for the collection
|
||||
* @param {array} [ids=[]] An array of ids (in correct order) for the
|
||||
* @param {Array} [ids=[]] An array of ids (in correct order) for the
|
||||
* model.
|
||||
* @param {Object} [response={}] An object containing the response from the
|
||||
* collection request.
|
||||
|
|
|
@ -55,7 +55,7 @@ const getCollectionHeaders = (
|
|||
* @param {Array} [ids=[]] Any ids for the collection request (these are
|
||||
* values that would be added to the route for a
|
||||
* route with id placeholders)
|
||||
* @return {array} an array of items stored in the collection.
|
||||
* @return {Array} an array of items stored in the collection.
|
||||
*/
|
||||
export const getCollection = (
|
||||
state,
|
||||
|
|
|
@ -147,7 +147,7 @@ const getRouteFromResourceEntries = ( stateSlice, ids = [] ) => {
|
|||
* For a given route, route parts and ids,
|
||||
*
|
||||
* @param {string} route
|
||||
* @param {array} routeParts
|
||||
* @param {Array} routeParts
|
||||
* @param {Array} ids
|
||||
*
|
||||
* @returns {string}
|
||||
|
|
|
@ -49,7 +49,7 @@ export const getRouteIds = ( route ) => {
|
|||
* /wc/blocks/products/attributes/{attribute_id}/terms/{id}
|
||||
*
|
||||
* @param {string} route The route to manipulate
|
||||
* @param {array} matchIds An array of named ids ( [ attribute_id, id ] )
|
||||
* @param {Array} matchIds An array of named ids ( [ attribute_id, id ] )
|
||||
*
|
||||
* @return {string} The route with new id placeholders
|
||||
*/
|
||||
|
|
|
@ -7,7 +7,7 @@ import { has } from 'lodash';
|
|||
* Utility for returning whether the given path exists in the state.
|
||||
*
|
||||
* @param {Object} state The state being checked
|
||||
* @param {array} path The path to check
|
||||
* @param {Array} path The path to check
|
||||
*
|
||||
* @return {bool} True means this exists in the state.
|
||||
*/
|
||||
|
|
|
@ -7,7 +7,7 @@ import { setWith, clone } from 'lodash';
|
|||
* Utility for updating state and only cloning objects in the path that changed.
|
||||
*
|
||||
* @param {Object} state The state being updated
|
||||
* @param {array} path The path being updated
|
||||
* @param {Array} path The path being updated
|
||||
* @param {*} value The value to update for the path
|
||||
*
|
||||
* @return {Object} The new state
|
||||
|
|
|
@ -11,7 +11,7 @@ import { allSettings } from './settings-init';
|
|||
* @param {mixed} [fallback=false] The value to use as a fallback
|
||||
* if the setting is not in the
|
||||
* state.
|
||||
* @param {function} [filter=( val ) => val] A callback for filtering the
|
||||
* @param {Function} [filter=( val ) => val] A callback for filtering the
|
||||
* value before it's returned.
|
||||
* Receives both the found value
|
||||
* (if it exists for the key) and
|
||||
|
|
|
@ -10,7 +10,7 @@ import { allSettings } from './settings-init';
|
|||
* @param {string} name The setting property key for the
|
||||
* setting being mutated.
|
||||
* @param {mixed} value The value to set.
|
||||
* @param {function} [filter=( val ) => val] Allows for providing a callback
|
||||
* @param {Function} [filter=( val ) => val] Allows for providing a callback
|
||||
* to sanitize the setting (eg.
|
||||
* ensure it's a number)
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { sortBy, map } from 'lodash';
|
||||
|
||||
/**
|
||||
* Given a query object, removes an attribute filter by a single slug.
|
||||
* @param {Array} query Current query object.
|
||||
* @param {Function} setQuery Callback to update the current query object.
|
||||
* @param {Object} attribute An attribute object.
|
||||
* @param {string} slug Term slug to remove.
|
||||
*/
|
||||
export const removeAttributeFilterBySlug = (
|
||||
query = [],
|
||||
setQuery = () => {},
|
||||
attribute,
|
||||
slug = ''
|
||||
) => {
|
||||
// Get current filter for provided attribute.
|
||||
const foundQuery = query.filter(
|
||||
( item ) => item.attribute === attribute.taxonomy
|
||||
);
|
||||
|
||||
const currentQuery = foundQuery.length ? foundQuery[ 0 ] : null;
|
||||
|
||||
if (
|
||||
! currentQuery ||
|
||||
! currentQuery.slug ||
|
||||
! Array.isArray( currentQuery.slug ) ||
|
||||
! currentQuery.slug.includes( slug )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newSlugs = currentQuery.slug.filter( ( item ) => item !== slug );
|
||||
|
||||
// Remove current attribute filter from query.
|
||||
const returnQuery = query.filter(
|
||||
( item ) => item.attribute !== attribute.taxonomy
|
||||
);
|
||||
|
||||
// Add a new query for selected terms, if provided.
|
||||
if ( newSlugs.length > 0 ) {
|
||||
currentQuery.slug = newSlugs.sort();
|
||||
returnQuery.push( currentQuery );
|
||||
}
|
||||
|
||||
setQuery( sortBy( returnQuery, 'attribute' ) );
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a query object, sets the query up to filter by a given attribute and attribute terms.
|
||||
* @param {Array} query Current query object.
|
||||
* @param {Function} setQuery Callback to update the current query object.
|
||||
* @param {Object} attribute An attribute object.
|
||||
* @param {Array} attributeTerms Array of term objects.
|
||||
* @param {string} operator Operator for the filter. Valid values: in, and.
|
||||
*/
|
||||
export const updateAttributeFilter = (
|
||||
query = [],
|
||||
setQuery = () => {},
|
||||
attribute,
|
||||
attributeTerms = [],
|
||||
operator = 'in'
|
||||
) => {
|
||||
const returnQuery = query.filter(
|
||||
( item ) => item.attribute !== attribute.taxonomy
|
||||
);
|
||||
|
||||
if ( attributeTerms.length === 0 ) {
|
||||
setQuery( returnQuery );
|
||||
} else {
|
||||
returnQuery.push( {
|
||||
attribute: attribute.taxonomy,
|
||||
operator,
|
||||
slug: map( attributeTerms, 'slug' ).sort(),
|
||||
} );
|
||||
setQuery( sortBy( returnQuery, 'attribute' ) );
|
||||
}
|
||||
};
|
|
@ -1,26 +1,77 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { find } from 'lodash';
|
||||
import { ATTRIBUTES } from '@woocommerce/block-settings';
|
||||
|
||||
/**
|
||||
* Get the ID of the first image attached to a product (the featured image).
|
||||
* Format an attribute from the settings into an object with standardized keys.
|
||||
* @param {Object} The attribute object.
|
||||
*/
|
||||
const attributeSettingToObject = ( attribute ) => {
|
||||
if ( ! attribute || ! attribute.attribute_name ) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: parseInt( attribute.attribute_id, 10 ),
|
||||
name: attribute.attribute_name,
|
||||
taxonomy: 'pa_' + attribute.attribute_name,
|
||||
label: attribute.attribute_label,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Format all attribute settings into objects.
|
||||
*/
|
||||
const attributeObjects = ATTRIBUTES.reduce( ( acc, current ) => {
|
||||
const attributeObject = attributeSettingToObject( current );
|
||||
|
||||
if ( attributeObject.id ) {
|
||||
acc.push( attributeObject );
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] );
|
||||
|
||||
/**
|
||||
* Get attribute data by taxonomy.
|
||||
*
|
||||
* @param {number} attributeId The attribute ID.
|
||||
* @return {Object|undefined} The attribute object if it exists.
|
||||
*/
|
||||
export const getAttributeFromID = ( attributeId ) => {
|
||||
if ( ! attributeId ) {
|
||||
return;
|
||||
}
|
||||
return attributeObjects.find( ( attribute ) => {
|
||||
return attribute.id === attributeId;
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get attribute data by taxonomy.
|
||||
*
|
||||
* @param {string} taxonomy The attribute taxonomy name e.g. pa_color.
|
||||
* @return {Object|undefined} The attribute object if it exists.
|
||||
*/
|
||||
export const getAttributeFromTaxonomy = ( taxonomy ) => {
|
||||
if ( ! taxonomy ) {
|
||||
return;
|
||||
}
|
||||
return attributeObjects.find( ( attribute ) => {
|
||||
return attribute.taxonomy === taxonomy;
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the taxonomy of an attribute by Attribute ID.
|
||||
*
|
||||
* @param {number} attributeId The attribute ID.
|
||||
* @return {string} The taxonomy name.
|
||||
*/
|
||||
export function getTaxonomyFromAttributeId( attributeId ) {
|
||||
export const getTaxonomyFromAttributeId = ( attributeId ) => {
|
||||
if ( ! attributeId ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const productAttribute = find( ATTRIBUTES, [
|
||||
'attribute_id',
|
||||
attributeId.toString(),
|
||||
] );
|
||||
|
||||
return productAttribute.attribute_name
|
||||
? 'pa_' + productAttribute.attribute_name
|
||||
: null;
|
||||
}
|
||||
const attribute = getAttributeFromID( attributeId );
|
||||
return attribute ? attribute.taxonomy : null;
|
||||
};
|
||||
|
|
|
@ -7,8 +7,8 @@ import { render } from 'react-dom';
|
|||
* Renders a block component in the place of a specified set of selectors.
|
||||
*
|
||||
* @param {string} selector CSS selector to match the elements to replace.
|
||||
* @param {function} Block React block to use as a replacement.
|
||||
* @param {function} [getProps] Function to generate the props object for the
|
||||
* @param {Function} Block React block to use as a replacement.
|
||||
* @param {Function} [getProps] Function to generate the props object for the
|
||||
* block.
|
||||
*/
|
||||
export default ( selector, Block, getProps = () => {} ) => {
|
||||
|
|
|
@ -72,6 +72,10 @@ const getAlias = ( options = {} ) => {
|
|||
__dirname,
|
||||
`../assets/js/${ pathPart }base/hooks/`
|
||||
),
|
||||
'@woocommerce/base-utils': path.resolve(
|
||||
__dirname,
|
||||
`../assets/js/${ pathPart }base/utils/`
|
||||
),
|
||||
'@woocommerce/block-components': path.resolve(
|
||||
__dirname,
|
||||
`../assets/js/${ pathPart }components/`
|
||||
|
@ -116,6 +120,7 @@ const mainEntry = {
|
|||
'all-products': './assets/js/blocks/products/all-products/index.js',
|
||||
'price-filter': './assets/js/blocks/price-filter/index.js',
|
||||
'attribute-filter': './assets/js/blocks/attribute-filter/index.js',
|
||||
'active-filters': './assets/js/blocks/active-filters/index.js',
|
||||
};
|
||||
|
||||
const frontEndEntry = {
|
||||
|
@ -123,6 +128,7 @@ const frontEndEntry = {
|
|||
'all-products': './assets/js/blocks/products/all-products/frontend.js',
|
||||
'price-filter': './assets/js/blocks/price-filter/frontend.js',
|
||||
'attribute-filter': './assets/js/blocks/attribute-filter/frontend.js',
|
||||
'active-filters': './assets/js/blocks/active-filters/frontend.js',
|
||||
};
|
||||
|
||||
const getEntryConfig = ( main = true, exclude = [] ) => {
|
||||
|
|
|
@ -4982,7 +4982,7 @@
|
|||
},
|
||||
"util": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
|
||||
"resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz",
|
||||
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -6322,7 +6322,7 @@
|
|||
},
|
||||
"browserify-aes": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
|
||||
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -6359,7 +6359,7 @@
|
|||
},
|
||||
"browserify-rsa": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
|
||||
"resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
|
||||
"integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -6572,7 +6572,7 @@
|
|||
"dependencies": {
|
||||
"callsites": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
|
||||
"integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=",
|
||||
"dev": true
|
||||
}
|
||||
|
@ -7339,7 +7339,7 @@
|
|||
},
|
||||
"create-hash": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
|
||||
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -7352,7 +7352,7 @@
|
|||
},
|
||||
"create-hmac": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
|
||||
"resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
|
||||
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -8082,7 +8082,7 @@
|
|||
},
|
||||
"diffie-hellman": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
|
||||
"resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
|
||||
"integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -10501,7 +10501,7 @@
|
|||
},
|
||||
"gettext-parser": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.4.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/gettext-parser/-/gettext-parser-1.4.0.tgz",
|
||||
"integrity": "sha512-sedZYLHlHeBop/gZ1jdg59hlUEcpcZJofLq2JFwJT1zTqAU3l2wFv6IsuwFHGqbiT9DWzMUW4/em2+hspnmMMA==",
|
||||
"requires": {
|
||||
"encoding": "^0.1.12",
|
||||
|
@ -11690,7 +11690,7 @@
|
|||
},
|
||||
"is-obj": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
|
||||
"resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
|
||||
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
|
||||
"dev": true
|
||||
},
|
||||
|
@ -13986,7 +13986,7 @@
|
|||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||
"resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -14892,7 +14892,7 @@
|
|||
},
|
||||
"os-homedir": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
|
||||
"resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
|
||||
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
|
||||
"dev": true
|
||||
},
|
||||
|
@ -14919,7 +14919,7 @@
|
|||
},
|
||||
"os-tmpdir": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||
"resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
|
||||
"dev": true
|
||||
},
|
||||
|
@ -15112,7 +15112,7 @@
|
|||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
||||
"dev": true
|
||||
},
|
||||
|
@ -17056,7 +17056,7 @@
|
|||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -17678,7 +17678,7 @@
|
|||
},
|
||||
"safe-regex": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
|
||||
"integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -18196,7 +18196,7 @@
|
|||
},
|
||||
"sha.js": {
|
||||
"version": "2.4.11",
|
||||
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
|
||||
"resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
|
||||
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -18902,7 +18902,7 @@
|
|||
},
|
||||
"strip-eof": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
|
||||
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
|
||||
"dev": true
|
||||
},
|
||||
|
@ -20300,6 +20300,11 @@
|
|||
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
|
||||
"dev": true
|
||||
},
|
||||
"use-debounce": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-3.1.0.tgz",
|
||||
"integrity": "sha512-DEf3L/ZKkOSTARk/DHlC6KAAJKwMqpck8Zx06SM2Wr+LfU1TzhO8hZRzB/qbpSQqREYWQes24n1q9doeTMqF4g=="
|
||||
},
|
||||
"util": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
|
||||
|
|
|
@ -111,7 +111,8 @@
|
|||
"eslint-plugin-woocommerce": "file:bin/eslint-plugin-woocommerce",
|
||||
"gridicons": "3.3.1",
|
||||
"react-number-format": "4.3.1",
|
||||
"trim-html": "0.1.9"
|
||||
"trim-html": "0.1.9",
|
||||
"use-debounce": "^3.1.0"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
|
|
@ -68,6 +68,7 @@ class Assets {
|
|||
self::register_script( 'wc-all-products', plugins_url( self::get_block_asset_build_path( 'all-products' ), __DIR__ ), $block_dependencies );
|
||||
self::register_script( 'wc-price-filter', plugins_url( self::get_block_asset_build_path( 'price-filter' ), __DIR__ ), $block_dependencies );
|
||||
self::register_script( 'wc-attribute-filter', plugins_url( self::get_block_asset_build_path( 'attribute-filter' ), __DIR__ ), $block_dependencies );
|
||||
self::register_script( 'wc-active-filters', plugins_url( self::get_block_asset_build_path( 'active-filters' ), __DIR__ ), $block_dependencies );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -116,7 +117,7 @@ class Assets {
|
|||
'showAvatars' => '1' === get_option( 'show_avatars' ),
|
||||
'enableReviewRating' => 'yes' === get_option( 'woocommerce_enable_review_rating' ),
|
||||
'productCount' => array_sum( (array) $product_counts ),
|
||||
'attributes' => wc_get_attribute_taxonomies(),
|
||||
'attributes' => array_values( wc_get_attribute_taxonomies() ),
|
||||
'wcBlocksAssetUrl' => plugins_url( 'assets/', __DIR__ ),
|
||||
]
|
||||
);
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
/**
|
||||
* Active filters block.
|
||||
*
|
||||
* @package WooCommerce/Blocks
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* ActiveFilters class.
|
||||
*/
|
||||
class ActiveFilters extends AbstractBlock {
|
||||
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'active-filters';
|
||||
|
||||
/**
|
||||
* Registers the block type with WordPress.
|
||||
*/
|
||||
public function register_block_type() {
|
||||
register_block_type(
|
||||
$this->namespace . '/' . $this->block_name,
|
||||
array(
|
||||
'render_callback' => array( $this, 'render' ),
|
||||
'editor_script' => 'wc-' . $this->block_name,
|
||||
'editor_style' => 'wc-block-editor',
|
||||
'style' => 'wc-block-style',
|
||||
'script' => 'wc-' . $this->block_name . '-frontend',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append frontend scripts when rendering the block.
|
||||
*
|
||||
* @param array $attributes Block attributes. Default empty array.
|
||||
* @param string $content Block content. Default empty string.
|
||||
* @return string Rendered block type output.
|
||||
*/
|
||||
public function render( $attributes = array(), $content = '' ) {
|
||||
\Automattic\WooCommerce\Blocks\Assets::register_block_script( $this->block_name . '-frontend' );
|
||||
return $content;
|
||||
}
|
||||
}
|
|
@ -38,7 +38,7 @@ class AttributeFilter extends AbstractBlock {
|
|||
}
|
||||
|
||||
/**
|
||||
* Append frontend scripts when rendering the Product Categories List block.
|
||||
* Append frontend scripts when rendering the block.
|
||||
*
|
||||
* @param array $attributes Block attributes. Default empty array.
|
||||
* @param string $content Block content. Default empty string.
|
||||
|
|
|
@ -49,6 +49,7 @@ class Library {
|
|||
$blocks[] = 'AllProducts';
|
||||
$blocks[] = 'PriceFilter';
|
||||
$blocks[] = 'AttributeFilter';
|
||||
$blocks[] = 'ActiveFilters';
|
||||
}
|
||||
foreach ( $blocks as $class ) {
|
||||
$class = __NAMESPACE__ . '\\BlockTypes\\' . $class;
|
||||
|
|
|
@ -106,7 +106,12 @@ const LegacyBlocksConfig = {
|
|||
getAlias( { pathPart: 'legacy' } )
|
||||
),
|
||||
],
|
||||
exclude: [ 'all-products', 'price-filter', 'attribute-filter' ],
|
||||
exclude: [
|
||||
'all-products',
|
||||
'price-filter',
|
||||
'attribute-filter',
|
||||
'active-filters',
|
||||
],
|
||||
} ),
|
||||
};
|
||||
|
||||
|
@ -122,7 +127,12 @@ const LegacyFrontendBlocksConfig = {
|
|||
getAlias( { pathPart: 'legacy' } )
|
||||
),
|
||||
],
|
||||
exclude: [ 'all-products', 'price-filter', 'attribute-filter' ],
|
||||
exclude: [
|
||||
'all-products',
|
||||
'price-filter',
|
||||
'attribute-filter',
|
||||
'active-filters',
|
||||
],
|
||||
} ),
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue