From 8cfef9c5dba583d76a30705277da57b89a3a938c Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Mon, 4 Jul 2022 17:34:10 +0200 Subject: [PATCH 01/10] First commit --- .../base/components/product-rating/index.tsx | 129 ++++++++++++++++++ .../base/components/product-rating/style.scss | 9 ++ .../hooks/collections/use-collection-data.ts | 18 +++ assets/js/blocks/rating-filter/attributes.ts | 11 ++ assets/js/blocks/rating-filter/block.json | 38 ++++++ assets/js/blocks/rating-filter/block.tsx | 121 ++++++++++++++++ assets/js/blocks/rating-filter/edit.tsx | 50 +++++++ assets/js/blocks/rating-filter/frontend.ts | 31 +++++ assets/js/blocks/rating-filter/index.tsx | 68 +++++++++ assets/js/blocks/rating-filter/types.ts | 14 ++ bin/webpack-entries.js | 1 + src/BlockTypes/RatingFilter.php | 15 ++ src/BlockTypesController.php | 1 + 13 files changed, 506 insertions(+) create mode 100644 assets/js/base/components/product-rating/index.tsx create mode 100644 assets/js/base/components/product-rating/style.scss create mode 100644 assets/js/blocks/rating-filter/attributes.ts create mode 100644 assets/js/blocks/rating-filter/block.json create mode 100644 assets/js/blocks/rating-filter/block.tsx create mode 100644 assets/js/blocks/rating-filter/edit.tsx create mode 100644 assets/js/blocks/rating-filter/frontend.ts create mode 100644 assets/js/blocks/rating-filter/index.tsx create mode 100644 assets/js/blocks/rating-filter/types.ts create mode 100644 src/BlockTypes/RatingFilter.php diff --git a/assets/js/base/components/product-rating/index.tsx b/assets/js/base/components/product-rating/index.tsx new file mode 100644 index 00000000000..ff932a83d70 --- /dev/null +++ b/assets/js/base/components/product-rating/index.tsx @@ -0,0 +1,129 @@ +/** + * External dependencies + */ +import classNames from 'classnames'; +import { __, _n, sprintf } from '@wordpress/i18n'; +import { useProductDataContext } from '@woocommerce/shared-context'; +import { Fragment } from '@wordpress/element'; +import { useStoreEvents } from '@woocommerce/base-context/hooks'; + +/** + * Internal dependencies + */ +import './style.scss'; + +/** @typedef {import('react')} React */ + +const Rating = ( { + averageRating, + className, + parentClassName, + productCount, + ratingCount, + showProductLink = true, + ...props +}: RatingProps ): JSX.Element => { + const ratingClassName = classNames( + 'wc-block-components-product-rating', + className + ); + + const starStyle = { + width: ( averageRating / 5 ) * 100 + '%', + }; + + const ratingText = sprintf( + /* translators: %f is referring to the average rating value */ + __( 'Rated %f out of 5', 'woo-gutenberg-products-block' ), + averageRating + ); + + const { dispatchStoreEvent } = useStoreEvents(); + + const { product } = useProductDataContext(); + + const ratingHTML = { + __html: sprintf( + /* translators: %1$s is referring to the average rating value, %2$s is referring to the number of ratings */ + _n( + 'Rated %1$s out of 5 based on %2$s customer rating', + 'Rated %1$s out of 5 based on %2$s customer ratings', + ratingCount, + 'woo-gutenberg-products-block' + ), + sprintf( '%f', averageRating ), + sprintf( '%d', ratingCount ) + ), + }; + + if ( ! product.id ) { + return ( +
+
+ +
+ { productCount ? `(${ productCount })` : null } +
+ ); + } + + const ParentComponent = showProductLink ? 'a' : Fragment; + + const anchorLabel = sprintf( + /* translators: %s is referring to the product name */ + __( 'Link to %s', 'woo-gutenberg-products-block' ), + product.name + ); + + const anchorProps = { + href: product.permalink, + ...{ 'aria-label': anchorLabel }, + onClick: () => { + dispatchStoreEvent( 'product-view-link', { + product, + } ); + }, + }; + + return ( + +
+
+ +
+ { productCount ? `(${ productCount })` : null } +
+
+ ); +}; + +interface RatingProps { + averageRating: 0 | 1 | 2 | 3 | 4 | 5; + className?: string; + parentClassName?: string; + productCount?: number; + ratingCount: number; + showProductLink: boolean; +} + +export default Rating; diff --git a/assets/js/base/components/product-rating/style.scss b/assets/js/base/components/product-rating/style.scss new file mode 100644 index 00000000000..deb3647cfa4 --- /dev/null +++ b/assets/js/base/components/product-rating/style.scss @@ -0,0 +1,9 @@ +.wc-block-components-product-rating { + text-decoration: none; + &__stars { + margin-left: unset; + display: inline-block; + line-height: 1; + height: 1em; + } +} diff --git a/assets/js/base/context/hooks/collections/use-collection-data.ts b/assets/js/base/context/hooks/collections/use-collection-data.ts index fdce755b002..27f8d923195 100644 --- a/assets/js/base/context/hooks/collections/use-collection-data.ts +++ b/assets/js/base/context/hooks/collections/use-collection-data.ts @@ -45,6 +45,7 @@ interface UseCollectionDataProps { }; queryPrices?: boolean; queryStock?: boolean; + queryRating?: boolean; queryState: Record< string, unknown >; } @@ -52,6 +53,7 @@ export const useCollectionData = ( { queryAttribute, queryPrices, queryStock, + queryRating, queryState, }: UseCollectionDataProps ) => { let context = useQueryStateContext(); @@ -66,10 +68,13 @@ export const useCollectionData = ( { calculateStockStatusQueryState, setCalculateStockStatusQueryState, ] = useQueryStateByKey( 'calculate_stock_status_counts', null, context ); + const [ calculateRatingQueryState, setCalculateRatingQueryState ] = + useQueryStateByKey( 'average_rating', null, context ); const currentQueryAttribute = useShallowEqual( queryAttribute || {} ); const currentQueryPrices = useShallowEqual( queryPrices ); const currentQueryStock = useShallowEqual( queryStock ); + const currentQueryRating = useShallowEqual( queryRating ); useEffect( () => { if ( @@ -124,6 +129,19 @@ export const useCollectionData = ( { calculateStockStatusQueryState, ] ); + useEffect( () => { + if ( + calculateRatingQueryState !== currentQueryRating && + currentQueryRating !== undefined + ) { + setCalculateRatingQueryState( currentQueryRating ); + } + }, [ + currentQueryRating, + setCalculateRatingQueryState, + calculateRatingQueryState, + ] ); + // Defer the select query so all collection-data query vars can be gathered. const [ shouldSelect, setShouldSelect ] = useState( false ); const [ debouncedShouldSelect ] = useDebounce( shouldSelect, 200 ); diff --git a/assets/js/blocks/rating-filter/attributes.ts b/assets/js/blocks/rating-filter/attributes.ts new file mode 100644 index 00000000000..6a3b3136423 --- /dev/null +++ b/assets/js/blocks/rating-filter/attributes.ts @@ -0,0 +1,11 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + +export const blockAttributes = { + heading: { + type: 'string', + default: __( 'Filter by rating', 'woo-gutenberg-products-block' ), + }, +}; diff --git a/assets/js/blocks/rating-filter/block.json b/assets/js/blocks/rating-filter/block.json new file mode 100644 index 00000000000..d947fb4a3b8 --- /dev/null +++ b/assets/js/blocks/rating-filter/block.json @@ -0,0 +1,38 @@ +{ + "name": "woocommerce/rating-filter", + "version": "1.0.0", + "title": "Filter Products by Rating", + "description": "Allow customers to filter the grid by products rating. Works in combination with the All Products block.", + "category": "woocommerce", + "keywords": [ "WooCommerce" ], + "supports": { + "html": false, + "multiple": false + }, + "example": { + "attributes": { + "isPreview": true + } + }, + "attributes": { + "className": { + "type": "string", + "default": "" + }, + "headingLevel": { + "type": "number", + "default": 3 + }, + "showFilterButton": { + "type": "boolean", + "default": false + }, + "isPreview": { + "type": "boolean", + "default": false + } + }, + "textdomain": "woo-gutenberg-products-block", + "apiVersion": 2, + "$schema": "https://schemas.wp.org/trunk/block.json" +} diff --git a/assets/js/blocks/rating-filter/block.tsx b/assets/js/blocks/rating-filter/block.tsx new file mode 100644 index 00000000000..85e11b6a786 --- /dev/null +++ b/assets/js/blocks/rating-filter/block.tsx @@ -0,0 +1,121 @@ +/** + * External dependencies + */ +// import { __, sprintf } from '@wordpress/i18n'; +// import { speak } from '@wordpress/a11y'; +// import { usePrevious, useShallowEqual } from '@woocommerce/base-hooks'; +// import { +// useQueryStateByKey, +// useQueryStateByContext, +// useCollectionData, +// } from '@woocommerce/base-context/hooks'; +// import { getSetting, getSettingWithCoercion } from '@woocommerce/settings'; +// import { +// useCallback, +// useEffect, +// useState, +// useMemo, +// useRef, +// } from '@wordpress/element'; +// import CheckboxList from '@woocommerce/base-components/checkbox-list'; +// import FilterSubmitButton from '@woocommerce/base-components/filter-submit-button'; +// import Label from '@woocommerce/base-components/filter-element-label'; +// import isShallowEqual from '@wordpress/is-shallow-equal'; +// import { decodeEntities } from '@wordpress/html-entities'; +// import { isBoolean, objectHasProp } from '@woocommerce/types'; +// import { addQueryArgs, removeQueryArgs } from '@wordpress/url'; +import { PREFIX_QUERY_ARG_FILTER_TYPE } from '@woocommerce/utils'; +import Rating from '@woocommerce/base-components/product-rating'; +import { + useQueryStateByKey, + useQueryStateByContext, + useCollectionData, +} from '@woocommerce/base-context/hooks'; + +/** + * Internal dependencies + */ +import { previewOptions } from './preview'; +// import './style.scss'; +import { Attributes } from './types'; + +export const QUERY_PARAM_KEY = PREFIX_QUERY_ARG_FILTER_TYPE + 'stock_status'; + +/** + * Component displaying an stock status filter. + * + * @param {Object} props Incoming props for the component. + * @param {Object} props.attributes Incoming block attributes. + * @param {boolean} props.isEditor + */ +const RatingFilterBlock = ( { + attributes: blockAttributes, + isEditor = false, +}: { + attributes: Attributes; + isEditor?: boolean; +} ) => { + const TagName = + `h${ blockAttributes.headingLevel }` as keyof JSX.IntrinsicElements; + + const [ queryState ] = useQueryStateByContext(); + const [ ratingProductQuery, setProductRatingQuery ] = useQueryStateByKey( + 'rating_counts', + [] + ); + + const { results: filteredCounts, isLoading: filteredCountsLoading } = + useCollectionData( { + queryRating: true, + queryState, + } ); + + console.log( filteredCounts ); + + return ( + <> + { ! isEditor && blockAttributes.heading && ( + + { blockAttributes.heading } + + ) } + + + + + + + ); +}; + +export default RatingFilterBlock; diff --git a/assets/js/blocks/rating-filter/edit.tsx b/assets/js/blocks/rating-filter/edit.tsx new file mode 100644 index 00000000000..ccc487df78e --- /dev/null +++ b/assets/js/blocks/rating-filter/edit.tsx @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import classnames from 'classnames'; +import { useBlockProps } from '@wordpress/block-editor'; +import { Disabled, withSpokenMessages } from '@wordpress/components'; +import BlockTitle from '@woocommerce/editor-components/block-title'; +import type { BlockEditProps } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import Block from './block'; +// import './editor.scss'; +import { Attributes } from './types'; + +const Edit = ( { + attributes, + setAttributes, +}: BlockEditProps< Attributes > ) => { + const { className, heading, headingLevel, showCounts, showFilterButton } = + attributes; + + const blockProps = useBlockProps( { + className: classnames( 'wc-block-rating-filter', className ), + } ); + + return ( + <> + { +
+ + setAttributes( { heading: value } ) + } + /> + + + +
+ } + + ); +}; + +export default withSpokenMessages( Edit ); diff --git a/assets/js/blocks/rating-filter/frontend.ts b/assets/js/blocks/rating-filter/frontend.ts new file mode 100644 index 00000000000..445628e40ca --- /dev/null +++ b/assets/js/blocks/rating-filter/frontend.ts @@ -0,0 +1,31 @@ +/** + * External dependencies + */ +import { renderFrontend } from '@woocommerce/base-utils'; + +/** + * Internal dependencies + */ +import Block from './block'; +import metadata from './block.json'; +import { blockAttributes } from './attributes'; + +const getProps = ( el: HTMLElement ) => { + return { + attributes: { + showCounts: el.dataset.showCounts === 'true', + heading: el.dataset.heading || blockAttributes.heading.default, + headingLevel: el.dataset.headingLevel + ? parseInt( el.dataset.headingLevel, 10 ) + : metadata.attributes.headingLevel.default, + showFilterButton: el.dataset.showFilterButton === 'true', + }, + isEditor: false, + }; +}; + +renderFrontend( { + selector: '.wp-block-woocommerce-rating-filter', + Block, + getProps, +} ); diff --git a/assets/js/blocks/rating-filter/index.tsx b/assets/js/blocks/rating-filter/index.tsx new file mode 100644 index 00000000000..f2211fce120 --- /dev/null +++ b/assets/js/blocks/rating-filter/index.tsx @@ -0,0 +1,68 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { registerBlockType } from '@wordpress/blocks'; +import { Icon, starEmpty } from '@wordpress/icons'; +import classNames from 'classnames'; +import { useBlockProps } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import edit from './edit'; +import metadata from './block.json'; +import { blockAttributes } from './attributes'; +import type { Attributes } from './types'; + +registerBlockType( metadata, { + title: __( 'Filter Products by Rating', 'woo-gutenberg-products-block' ), + description: __( + 'Allow customers to filter the grid by products rating. Works in combination with the All Products block.', + 'woo-gutenberg-products-block' + ), + icon: { + src: ( + + ), + }, + attributes: { + ...metadata.attributes, + ...blockAttributes, + }, + edit, + // Save the props to post content. + save( { attributes }: { attributes: Attributes } ) { + const { + className, + showCounts, + heading, + headingLevel, + showFilterButton, + } = attributes; + const data: Record< string, unknown > = { + 'data-show-counts': showCounts, + 'data-heading': heading, + 'data-heading-level': headingLevel, + }; + if ( showFilterButton ) { + data[ 'data-show-filter-button' ] = showFilterButton; + } + return ( +
+ +
+ ); + }, +} ); diff --git a/assets/js/blocks/rating-filter/types.ts b/assets/js/blocks/rating-filter/types.ts new file mode 100644 index 00000000000..99d3ba679a2 --- /dev/null +++ b/assets/js/blocks/rating-filter/types.ts @@ -0,0 +1,14 @@ +export interface Attributes { + className?: string; + heading: string; + headingLevel: number; + showCounts: boolean; + showFilterButton: boolean; + isPreview?: boolean; +} + +export interface DisplayOption { + value: string; + name: string; + label: JSX.Element; +} diff --git a/bin/webpack-entries.js b/bin/webpack-entries.js index b6e9d239f15..d34818526b1 100644 --- a/bin/webpack-entries.js +++ b/bin/webpack-entries.js @@ -44,6 +44,7 @@ const blocks = { 'attribute-filter': {}, 'stock-filter': {}, 'active-filters': {}, + 'rating-filter': {}, cart: {}, checkout: {}, 'mini-cart': {}, diff --git a/src/BlockTypes/RatingFilter.php b/src/BlockTypes/RatingFilter.php new file mode 100644 index 00000000000..b467ef8f95f --- /dev/null +++ b/src/BlockTypes/RatingFilter.php @@ -0,0 +1,15 @@ + Date: Fri, 29 Jul 2022 15:30:56 +0200 Subject: [PATCH 02/10] Add interactivity to the Product by Rating filter block --- .../components/product-list/product-list.tsx | 7 + .../base/components/product-rating/index.tsx | 57 ++-- .../hooks/collections/use-collection-data.ts | 2 +- assets/js/blocks/active-filters/block.tsx | 2 +- assets/js/blocks/rating-filter/block.tsx | 261 +++++++++++++----- assets/js/blocks/rating-filter/style.scss | 8 + assets/js/blocks/rating-filter/utils.ts | 24 ++ 7 files changed, 264 insertions(+), 97 deletions(-) create mode 100644 assets/js/blocks/rating-filter/style.scss create mode 100644 assets/js/blocks/rating-filter/utils.ts diff --git a/assets/js/base/components/product-list/product-list.tsx b/assets/js/base/components/product-list/product-list.tsx index c2d0ca4b2e0..94704365ea5 100644 --- a/assets/js/base/components/product-list/product-list.tsx +++ b/assets/js/base/components/product-list/product-list.tsx @@ -132,6 +132,11 @@ const ProductList = ( { 'stock_status', [] ); + const [ productRating, setProductRating ] = useQueryStateByKey( + 'rating', + [] + ); + const [ minPrice, setMinPrice ] = useQueryStateByKey( 'min_price' ); const [ maxPrice, setMaxPrice ] = useQueryStateByKey( 'max_price' ); @@ -215,6 +220,7 @@ const ProductList = ( { const hasFilters = productAttributes.length > 0 || productStockStatus.length > 0 || + productRating.length > 0 || Number.isFinite( minPrice ) || Number.isFinite( maxPrice ); @@ -231,6 +237,7 @@ const ProductList = ( { resetCallback={ () => { setProductAttributes( [] ); setProductStockStatus( [] ); + setProductRating( [] ); setMinPrice( null ); setMaxPrice( null ); } } diff --git a/assets/js/base/components/product-rating/index.tsx b/assets/js/base/components/product-rating/index.tsx index ff932a83d70..a4229d21d09 100644 --- a/assets/js/base/components/product-rating/index.tsx +++ b/assets/js/base/components/product-rating/index.tsx @@ -15,12 +15,13 @@ import './style.scss'; /** @typedef {import('react')} React */ const Rating = ( { - averageRating, + rating, className, parentClassName, productCount, ratingCount, - showProductLink = true, + ratedProductsCount, + showProductLink = false, ...props }: RatingProps ): JSX.Element => { const ratingClassName = classNames( @@ -29,32 +30,50 @@ const Rating = ( { ); const starStyle = { - width: ( averageRating / 5 ) * 100 + '%', + width: ( rating / 5 ) * 100 + '%', }; const ratingText = sprintf( /* translators: %f is referring to the average rating value */ __( 'Rated %f out of 5', 'woo-gutenberg-products-block' ), - averageRating + rating ); const { dispatchStoreEvent } = useStoreEvents(); const { product } = useProductDataContext(); - const ratingHTML = { - __html: sprintf( - /* translators: %1$s is referring to the average rating value, %2$s is referring to the number of ratings */ - _n( - 'Rated %1$s out of 5 based on %2$s customer rating', - 'Rated %1$s out of 5 based on %2$s customer ratings', - ratingCount, - 'woo-gutenberg-products-block' + let ratingHTML = ''; + + if ( ratedProductsCount ) { + ratingHTML = { + __html: sprintf( + /* translators: %1$s is referring to the average rating value, %2$s is referring to the number of ratings */ + _n( + 'Rated %1$s out of 5 based on %2$s customer rating', + 'Rated %1$s out of 5 based on %2$s customer ratings', + ratingCount, + 'woo-gutenberg-products-block' + ), + sprintf( '%f', rating ), + sprintf( '%d', ratingCount ) ), - sprintf( '%f', averageRating ), - sprintf( '%d', ratingCount ) - ), - }; + }; + } else { + ratingHTML = { + __html: sprintf( + /* translators: %1$s is referring to the rating value */ + _n( + 'Rated %1$s out of 5', + 'Rated %1$s out of 5', + ratingCount, + 'woo-gutenberg-products-block' + ), + sprintf( '%f', rating ), + ratedProductsCount + ), + }; + } if ( ! product.id ) { return ( @@ -72,13 +91,12 @@ const Rating = ( { dangerouslySetInnerHTML={ ratingHTML } /> - { productCount ? `(${ productCount })` : null } + { ratedProductsCount ? `(${ ratedProductsCount })` : null } ); } const ParentComponent = showProductLink ? 'a' : Fragment; - const anchorLabel = sprintf( /* translators: %s is referring to the product name */ __( 'Link to %s', 'woo-gutenberg-products-block' ), @@ -118,11 +136,12 @@ const Rating = ( { }; interface RatingProps { - averageRating: 0 | 1 | 2 | 3 | 4 | 5; + rating: 0 | 1 | 2 | 3 | 4 | 5; className?: string; parentClassName?: string; productCount?: number; ratingCount: number; + ratedProductsCount: number; showProductLink: boolean; } diff --git a/assets/js/base/context/hooks/collections/use-collection-data.ts b/assets/js/base/context/hooks/collections/use-collection-data.ts index 27f8d923195..98c16047f9e 100644 --- a/assets/js/base/context/hooks/collections/use-collection-data.ts +++ b/assets/js/base/context/hooks/collections/use-collection-data.ts @@ -69,7 +69,7 @@ export const useCollectionData = ( { setCalculateStockStatusQueryState, ] = useQueryStateByKey( 'calculate_stock_status_counts', null, context ); const [ calculateRatingQueryState, setCalculateRatingQueryState ] = - useQueryStateByKey( 'average_rating', null, context ); + useQueryStateByKey( 'calculate_rating_counts', null, context ); const currentQueryAttribute = useShallowEqual( queryAttribute || {} ); const currentQueryPrices = useShallowEqual( queryPrices ); diff --git a/assets/js/blocks/active-filters/block.tsx b/assets/js/blocks/active-filters/block.tsx index 12b38f87d8a..c00ac6f7549 100644 --- a/assets/js/blocks/active-filters/block.tsx +++ b/assets/js/blocks/active-filters/block.tsx @@ -151,7 +151,7 @@ const ActiveFiltersBlock = ( { }, [ productAttributes, blockAttributes.displayStyle ] ); const [ productRatings, setProductRatings ] = - useQueryStateByKey( 'ratings' ); + useQueryStateByKey( 'rating' ); /** * Parse the filter URL to set the active rating fitlers. diff --git a/assets/js/blocks/rating-filter/block.tsx b/assets/js/blocks/rating-filter/block.tsx index 85e11b6a786..475caedb303 100644 --- a/assets/js/blocks/rating-filter/block.tsx +++ b/assets/js/blocks/rating-filter/block.tsx @@ -1,45 +1,33 @@ /** * External dependencies */ -// import { __, sprintf } from '@wordpress/i18n'; -// import { speak } from '@wordpress/a11y'; -// import { usePrevious, useShallowEqual } from '@woocommerce/base-hooks'; -// import { -// useQueryStateByKey, -// useQueryStateByContext, -// useCollectionData, -// } from '@woocommerce/base-context/hooks'; -// import { getSetting, getSettingWithCoercion } from '@woocommerce/settings'; -// import { -// useCallback, -// useEffect, -// useState, -// useMemo, -// useRef, -// } from '@wordpress/element'; -// import CheckboxList from '@woocommerce/base-components/checkbox-list'; -// import FilterSubmitButton from '@woocommerce/base-components/filter-submit-button'; -// import Label from '@woocommerce/base-components/filter-element-label'; -// import isShallowEqual from '@wordpress/is-shallow-equal'; -// import { decodeEntities } from '@wordpress/html-entities'; -// import { isBoolean, objectHasProp } from '@woocommerce/types'; -// import { addQueryArgs, removeQueryArgs } from '@wordpress/url'; -import { PREFIX_QUERY_ARG_FILTER_TYPE } from '@woocommerce/utils'; import Rating from '@woocommerce/base-components/product-rating'; +import { speak } from '@wordpress/a11y'; +import { usePrevious, useShallowEqual } from '@woocommerce/base-hooks'; +import LoadingMask from '@woocommerce/base-components/loading-mask'; import { useQueryStateByKey, useQueryStateByContext, useCollectionData, } from '@woocommerce/base-context/hooks'; +import isRatingQueryCollection from '@woocommerce/types'; +import { getSetting, getSettingWithCoercion } from '@woocommerce/settings'; +import { isBoolean } from '@woocommerce/types'; +import { sortBy } from 'lodash'; +import isShallowEqual from '@wordpress/is-shallow-equal'; +import { useState, useCallback, useMemo, useEffect } from '@wordpress/element'; +import { addQueryArgs, removeQueryArgs } from '@wordpress/url'; +import { changeUrl, PREFIX_QUERY_ARG_FILTER_TYPE } from '@woocommerce/utils'; /** * Internal dependencies */ -import { previewOptions } from './preview'; -// import './style.scss'; +// import { previewOptions } from './preview'; +import './style.scss'; import { Attributes } from './types'; +import { getActiveFilters } from './utils'; -export const QUERY_PARAM_KEY = PREFIX_QUERY_ARG_FILTER_TYPE + 'stock_status'; +export const QUERY_PARAM_KEY = PREFIX_QUERY_ARG_FILTER_TYPE + 'rating'; /** * Component displaying an stock status filter. @@ -55,14 +43,19 @@ const RatingFilterBlock = ( { attributes: Attributes; isEditor?: boolean; } ) => { + const filteringForPhpTemplate = getSettingWithCoercion( + 'is_rendering_php_template', + false, + isBoolean + ); + + const [ hasSetFilterDefaultsFromUrl, setHasSetFilterDefaultsFromUrl ] = + useState( false ); + const TagName = `h${ blockAttributes.headingLevel }` as keyof JSX.IntrinsicElements; const [ queryState ] = useQueryStateByContext(); - const [ ratingProductQuery, setProductRatingQuery ] = useQueryStateByKey( - 'rating_counts', - [] - ); const { results: filteredCounts, isLoading: filteredCountsLoading } = useCollectionData( { @@ -70,52 +63,168 @@ const RatingFilterBlock = ( { queryState, } ); - console.log( filteredCounts ); - - return ( - <> - { ! isEditor && blockAttributes.heading && ( - - { blockAttributes.heading } - - ) } - - - - - - + const RATING_OPTIONS = getSetting( 'ratingOptions', [] ); + + const initialFilters = useMemo( + () => getActiveFilters( RATING_OPTIONS.current, QUERY_PARAM_KEY ), + [] + ); + + const [ clicked, setClicked ] = useState( initialFilters ); + + const [ displayedOptions, setDisplayedOptions ] = useState( + blockAttributes.isPreview ? previewOptions : [] + ); + + const [ productRatings, setProductRatings ] = + useQueryStateByKey( 'rating' ); + + const [ productRatingsQuery, setProductRatingsQuery ] = useQueryStateByKey( + 'rating', + initialFilters ); + + const productRatingsArray = Array.from( productRatings ); + + /** + * Used to redirect the page when filters are changed so templates using the Classic Template block can filter. + * + * @param {Array} productRatingsArray Array of checked stock options. + */ + const updateFilterUrl = ( productRatingsArray: string[] ) => { + if ( ! window ) { + return; + } + + if ( productRatingsArray.length === 0 ) { + const url = removeQueryArgs( + window.location.href, + QUERY_PARAM_KEY + ); + + if ( url !== window.location.href ) { + changeUrl( url ); + } + + return; + } + + const newUrl = addQueryArgs( window.location.href, { + [ QUERY_PARAM_KEY ]: productRatingsArray.join( ',' ), + } ); + + if ( newUrl === window.location.href ) { + return; + } + + changeUrl( newUrl ); + }; + + const onSubmit = useCallback( + ( isClicked ) => { + if ( isEditor ) { + return; + } + if ( isClicked && ! filteringForPhpTemplate ) { + setProductRatingsQuery( clicked ); + } + + updateFilterUrl( clicked ); + }, + [ isEditor, setProductRatingsQuery, clicked, filteringForPhpTemplate ] + ); + + // Track clicked STATE changes - if state changes, update the query. + useEffect( () => { + if ( ! blockAttributes.showFilterButton ) { + onSubmit( clicked ); + } + }, [ blockAttributes.showFilterButton, clicked, onSubmit ] ); + + const clickedQuery = useMemo( () => { + return productRatingsQuery; + }, [ productRatingsQuery ] ); + + const currentClickedQuery = useShallowEqual( clickedQuery ); + const previousClickedQuery = usePrevious( currentClickedQuery ); + // Track Stock query changes so the block reflects current filters. + useEffect( () => { + if ( + ! isShallowEqual( previousClickedQuery, currentClickedQuery ) && // Clicked query changed. + ! isShallowEqual( clicked, currentClickedQuery ) // Clicked query doesn't match the UI. + ) { + setClicked( currentClickedQuery ); + } + }, [ clicked, currentClickedQuery, previousClickedQuery ] ); + + /** + * Try get the stock filter from the URL. + */ + useEffect( () => { + if ( ! hasSetFilterDefaultsFromUrl ) { + setProductRatings( initialFilters ); + setHasSetFilterDefaultsFromUrl( true ); + } + }, [ + setProductRatings, + hasSetFilterDefaultsFromUrl, + setHasSetFilterDefaultsFromUrl, + initialFilters, + ] ); + + const onClick = ( clickedValue ) => () => { + if ( ! productRatingsArray.length ) { + setProductRatings( [ clickedValue ] ); + } else { + const previouslyClicked = + productRatingsArray.includes( clickedValue ); + const newClicked = productRatingsArray.filter( + ( value ) => value !== clickedValue + ); + if ( ! previouslyClicked ) { + newClicked.push( clickedValue ); + newClicked.sort(); + } + setProductRatings( newClicked ); + } + }; + + if ( filteredCounts.rating_counts != undefined ) { + const orderedRatings = [ ...filteredCounts.rating_counts ].reverse(); + return ( + <> + { ! isEditor && blockAttributes.heading && ( + + { blockAttributes.heading } + + ) } + { orderedRatings.map( ( item ) => ( + + ) ) } + { blockAttributes.showFilterButton && ( + onSubmit( checked ) } + /> + ) } + + ); + } + return ; }; export default RatingFilterBlock; diff --git a/assets/js/blocks/rating-filter/style.scss b/assets/js/blocks/rating-filter/style.scss new file mode 100644 index 00000000000..c96c6142d2a --- /dev/null +++ b/assets/js/blocks/rating-filter/style.scss @@ -0,0 +1,8 @@ +.wp-block-woocommerce-rating-filter { + margin-bottom: $gap-large; + + .wc-block-components-product-rating.is-active { + opacity: 0.35; + } +} + diff --git a/assets/js/blocks/rating-filter/utils.ts b/assets/js/blocks/rating-filter/utils.ts new file mode 100644 index 00000000000..c4511b2a6fa --- /dev/null +++ b/assets/js/blocks/rating-filter/utils.ts @@ -0,0 +1,24 @@ +/** + * External dependencies + */ +import { isString } from '@woocommerce/types'; +import { getUrlParameter } from '@woocommerce/utils'; + +export const getActiveFilters = ( + filters: Record< string, string >, + queryParamKey = 'rating_filter' +) => { + const params = getUrlParameter( queryParamKey ); + + if ( ! params ) { + return []; + } + + const parsedParams = isString( params ) + ? params.split( ',' ) + : ( params as string[] ); + + return Object.keys( filters ).filter( ( filter ) => + parsedParams.includes( filter ) + ); +}; From 28dff811fb9b1e9b03135bb6df6146cff0fc748d Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Thu, 25 Aug 2022 16:16:40 +0200 Subject: [PATCH 03/10] Fix block with the latest repo changes --- assets/js/blocks/rating-filter/block.json | 4 ++ assets/js/blocks/rating-filter/block.tsx | 12 ++---- assets/js/blocks/rating-filter/edit.tsx | 52 ++++++++++++++++++++--- assets/js/blocks/rating-filter/utils.ts | 9 +--- 4 files changed, 56 insertions(+), 21 deletions(-) diff --git a/assets/js/blocks/rating-filter/block.json b/assets/js/blocks/rating-filter/block.json index d947fb4a3b8..af5ac0f3a73 100644 --- a/assets/js/blocks/rating-filter/block.json +++ b/assets/js/blocks/rating-filter/block.json @@ -23,6 +23,10 @@ "type": "number", "default": 3 }, + "showCounts": { + "type": "boolean", + "default": true + }, "showFilterButton": { "type": "boolean", "default": false diff --git a/assets/js/blocks/rating-filter/block.tsx b/assets/js/blocks/rating-filter/block.tsx index 475caedb303..d311bc51bb7 100644 --- a/assets/js/blocks/rating-filter/block.tsx +++ b/assets/js/blocks/rating-filter/block.tsx @@ -2,7 +2,6 @@ * External dependencies */ import Rating from '@woocommerce/base-components/product-rating'; -import { speak } from '@wordpress/a11y'; import { usePrevious, useShallowEqual } from '@woocommerce/base-hooks'; import LoadingMask from '@woocommerce/base-components/loading-mask'; import { @@ -10,10 +9,8 @@ import { useQueryStateByContext, useCollectionData, } from '@woocommerce/base-context/hooks'; -import isRatingQueryCollection from '@woocommerce/types'; -import { getSetting, getSettingWithCoercion } from '@woocommerce/settings'; +import { getSettingWithCoercion } from '@woocommerce/settings'; import { isBoolean } from '@woocommerce/types'; -import { sortBy } from 'lodash'; import isShallowEqual from '@wordpress/is-shallow-equal'; import { useState, useCallback, useMemo, useEffect } from '@wordpress/element'; import { addQueryArgs, removeQueryArgs } from '@wordpress/url'; @@ -22,7 +19,6 @@ import { changeUrl, PREFIX_QUERY_ARG_FILTER_TYPE } from '@woocommerce/utils'; /** * Internal dependencies */ -// import { previewOptions } from './preview'; import './style.scss'; import { Attributes } from './types'; import { getActiveFilters } from './utils'; @@ -49,6 +45,8 @@ const RatingFilterBlock = ( { isBoolean ); + console.log(PREFIX_QUERY_ARG_FILTER_TYPE + 'rating'); + const [ hasSetFilterDefaultsFromUrl, setHasSetFilterDefaultsFromUrl ] = useState( false ); @@ -63,10 +61,8 @@ const RatingFilterBlock = ( { queryState, } ); - const RATING_OPTIONS = getSetting( 'ratingOptions', [] ); - const initialFilters = useMemo( - () => getActiveFilters( RATING_OPTIONS.current, QUERY_PARAM_KEY ), + () => getActiveFilters( 'filter_rating', QUERY_PARAM_KEY ), [] ); diff --git a/assets/js/blocks/rating-filter/edit.tsx b/assets/js/blocks/rating-filter/edit.tsx index ccc487df78e..6714fc3da7b 100644 --- a/assets/js/blocks/rating-filter/edit.tsx +++ b/assets/js/blocks/rating-filter/edit.tsx @@ -3,8 +3,14 @@ */ import { __ } from '@wordpress/i18n'; import classnames from 'classnames'; -import { useBlockProps } from '@wordpress/block-editor'; -import { Disabled, withSpokenMessages } from '@wordpress/components'; +import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; +import { + Disabled, + PanelBody, + ToggleControl, + withSpokenMessages, +} from '@wordpress/components'; +import HeadingToolbar from '@woocommerce/editor-components/heading-toolbar'; import BlockTitle from '@woocommerce/editor-components/block-title'; import type { BlockEditProps } from '@wordpress/blocks'; @@ -19,19 +25,53 @@ const Edit = ( { attributes, setAttributes, }: BlockEditProps< Attributes > ) => { - const { className, heading, headingLevel, showCounts, showFilterButton } = - attributes; + const { className, heading, headingLevel, showCounts } = attributes; const blockProps = useBlockProps( { - className: classnames( 'wc-block-rating-filter', className ), + className: classnames( 'wc-block-stock-filter', className ), } ); + const getInspectorControls = () => { + return ( + + + + setAttributes( { + showCounts: ! showCounts, + } ) + } + /> + + + ); + }; + return ( <> + { getInspectorControls() } {
diff --git a/assets/js/blocks/rating-filter/utils.ts b/assets/js/blocks/rating-filter/utils.ts index c4511b2a6fa..71b053db920 100644 --- a/assets/js/blocks/rating-filter/utils.ts +++ b/assets/js/blocks/rating-filter/utils.ts @@ -4,10 +4,7 @@ import { isString } from '@woocommerce/types'; import { getUrlParameter } from '@woocommerce/utils'; -export const getActiveFilters = ( - filters: Record< string, string >, - queryParamKey = 'rating_filter' -) => { +export const getActiveFilters = ( queryParamKey = 'filter_rating' ) => { const params = getUrlParameter( queryParamKey ); if ( ! params ) { @@ -18,7 +15,5 @@ export const getActiveFilters = ( ? params.split( ',' ) : ( params as string[] ); - return Object.keys( filters ).filter( ( filter ) => - parsedParams.includes( filter ) - ); + return parsedParams; }; From 602f563698afca7d8086db9e24f44d85a2995a52 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Sat, 3 Sep 2022 08:27:08 +0200 Subject: [PATCH 04/10] Product by Rating: Code tidying up --- .../base/components/product-rating/index.tsx | 139 +++++------------- assets/js/blocks/rating-filter/block.json | 8 - assets/js/blocks/rating-filter/block.tsx | 51 +++---- assets/js/blocks/rating-filter/edit.tsx | 53 +------ assets/js/blocks/rating-filter/frontend.ts | 2 - assets/js/blocks/rating-filter/index.tsx | 12 +- assets/js/blocks/rating-filter/style.scss | 8 +- assets/js/blocks/rating-filter/types.ts | 8 - 8 files changed, 65 insertions(+), 216 deletions(-) diff --git a/assets/js/base/components/product-rating/index.tsx b/assets/js/base/components/product-rating/index.tsx index a4229d21d09..07225d75082 100644 --- a/assets/js/base/components/product-rating/index.tsx +++ b/assets/js/base/components/product-rating/index.tsx @@ -3,26 +3,18 @@ */ import classNames from 'classnames'; import { __, _n, sprintf } from '@wordpress/i18n'; -import { useProductDataContext } from '@woocommerce/shared-context'; -import { Fragment } from '@wordpress/element'; -import { useStoreEvents } from '@woocommerce/base-context/hooks'; /** * Internal dependencies */ import './style.scss'; -/** @typedef {import('react')} React */ - const Rating = ( { - rating, className, - parentClassName, - productCount, - ratingCount, + key, + rating, ratedProductsCount, - showProductLink = false, - ...props + onClick, }: RatingProps ): JSX.Element => { const ratingClassName = classNames( 'wc-block-components-product-rating', @@ -39,110 +31,47 @@ const Rating = ( { rating ); - const { dispatchStoreEvent } = useStoreEvents(); - - const { product } = useProductDataContext(); - - let ratingHTML = ''; - - if ( ratedProductsCount ) { - ratingHTML = { - __html: sprintf( - /* translators: %1$s is referring to the average rating value, %2$s is referring to the number of ratings */ - _n( - 'Rated %1$s out of 5 based on %2$s customer rating', - 'Rated %1$s out of 5 based on %2$s customer ratings', - ratingCount, - 'woo-gutenberg-products-block' - ), - sprintf( '%f', rating ), - sprintf( '%d', ratingCount ) - ), - }; - } else { - ratingHTML = { - __html: sprintf( - /* translators: %1$s is referring to the rating value */ - _n( - 'Rated %1$s out of 5', - 'Rated %1$s out of 5', - ratingCount, - 'woo-gutenberg-products-block' - ), - sprintf( '%f', rating ), - ratedProductsCount + const ratingHTML = { + __html: sprintf( + /* translators: %1$s is referring to the rating value */ + _n( + 'Rated %1$s out of 5', + 'Rated %1$s out of 5', + ratedProductsCount, + 'woo-gutenberg-products-block' ), - }; - } - - if ( ! product.id ) { - return ( -
-
- -
- { ratedProductsCount ? `(${ ratedProductsCount })` : null } -
- ); - } - - const ParentComponent = showProductLink ? 'a' : Fragment; - const anchorLabel = sprintf( - /* translators: %s is referring to the product name */ - __( 'Link to %s', 'woo-gutenberg-products-block' ), - product.name - ); - - const anchorProps = { - href: product.permalink, - ...{ 'aria-label': anchorLabel }, - onClick: () => { - dispatchStoreEvent( 'product-view-link', { - product, - } ); - }, + sprintf( '%f', rating ) + ), }; return ( - -
-
- -
- { productCount ? `(${ productCount })` : null } +
+
+
- + { ratedProductsCount ? `(${ ratedProductsCount })` : null } +
); }; - interface RatingProps { + className: string; + key: 0 | 1 | 2 | 3 | 4 | 5; rating: 0 | 1 | 2 | 3 | 4 | 5; - className?: string; - parentClassName?: string; - productCount?: number; - ratingCount: number; ratedProductsCount: number; - showProductLink: boolean; + onClick: () => void; } export default Rating; diff --git a/assets/js/blocks/rating-filter/block.json b/assets/js/blocks/rating-filter/block.json index af5ac0f3a73..1bc39733306 100644 --- a/assets/js/blocks/rating-filter/block.json +++ b/assets/js/blocks/rating-filter/block.json @@ -23,14 +23,6 @@ "type": "number", "default": 3 }, - "showCounts": { - "type": "boolean", - "default": true - }, - "showFilterButton": { - "type": "boolean", - "default": false - }, "isPreview": { "type": "boolean", "default": false diff --git a/assets/js/blocks/rating-filter/block.tsx b/assets/js/blocks/rating-filter/block.tsx index d311bc51bb7..6f1ea970fdd 100644 --- a/assets/js/blocks/rating-filter/block.tsx +++ b/assets/js/blocks/rating-filter/block.tsx @@ -3,7 +3,6 @@ */ import Rating from '@woocommerce/base-components/product-rating'; import { usePrevious, useShallowEqual } from '@woocommerce/base-hooks'; -import LoadingMask from '@woocommerce/base-components/loading-mask'; import { useQueryStateByKey, useQueryStateByContext, @@ -14,7 +13,7 @@ import { isBoolean } from '@woocommerce/types'; import isShallowEqual from '@wordpress/is-shallow-equal'; import { useState, useCallback, useMemo, useEffect } from '@wordpress/element'; import { addQueryArgs, removeQueryArgs } from '@wordpress/url'; -import { changeUrl, PREFIX_QUERY_ARG_FILTER_TYPE } from '@woocommerce/utils'; +import { changeUrl } from '@woocommerce/utils'; /** * Internal dependencies @@ -23,7 +22,7 @@ import './style.scss'; import { Attributes } from './types'; import { getActiveFilters } from './utils'; -export const QUERY_PARAM_KEY = PREFIX_QUERY_ARG_FILTER_TYPE + 'rating'; +export const QUERY_PARAM_KEY = 'rating_filter'; /** * Component displaying an stock status filter. @@ -44,9 +43,6 @@ const RatingFilterBlock = ( { false, isBoolean ); - - console.log(PREFIX_QUERY_ARG_FILTER_TYPE + 'rating'); - const [ hasSetFilterDefaultsFromUrl, setHasSetFilterDefaultsFromUrl ] = useState( false ); @@ -62,16 +58,12 @@ const RatingFilterBlock = ( { } ); const initialFilters = useMemo( - () => getActiveFilters( 'filter_rating', QUERY_PARAM_KEY ), + () => getActiveFilters( 'rating_filter' ), [] ); const [ clicked, setClicked ] = useState( initialFilters ); - const [ displayedOptions, setDisplayedOptions ] = useState( - blockAttributes.isPreview ? previewOptions : [] - ); - const [ productRatings, setProductRatings ] = useQueryStateByKey( 'rating' ); @@ -80,19 +72,19 @@ const RatingFilterBlock = ( { initialFilters ); - const productRatingsArray = Array.from( productRatings ); + const productRatingsArray: string[] = Array.from( productRatings ); /** * Used to redirect the page when filters are changed so templates using the Classic Template block can filter. * - * @param {Array} productRatingsArray Array of checked stock options. + * @param {Array} clickedRatings Array of clicked ratings. */ - const updateFilterUrl = ( productRatingsArray: string[] ) => { + const updateFilterUrl = ( clickedRatings: string[] ) => { if ( ! window ) { return; } - if ( productRatingsArray.length === 0 ) { + if ( clickedRatings.length === 0 ) { const url = removeQueryArgs( window.location.href, QUERY_PARAM_KEY @@ -106,7 +98,7 @@ const RatingFilterBlock = ( { } const newUrl = addQueryArgs( window.location.href, { - [ QUERY_PARAM_KEY ]: productRatingsArray.join( ',' ), + [ QUERY_PARAM_KEY ]: clickedRatings.join( ',' ), } ); if ( newUrl === window.location.href ) { @@ -132,10 +124,8 @@ const RatingFilterBlock = ( { // Track clicked STATE changes - if state changes, update the query. useEffect( () => { - if ( ! blockAttributes.showFilterButton ) { - onSubmit( clicked ); - } - }, [ blockAttributes.showFilterButton, clicked, onSubmit ] ); + onSubmit( clicked ); + }, [ clicked, onSubmit ] ); const clickedQuery = useMemo( () => { return productRatingsQuery; @@ -154,7 +144,7 @@ const RatingFilterBlock = ( { }, [ clicked, currentClickedQuery, previousClickedQuery ] ); /** - * Try get the stock filter from the URL. + * Try get the rating filter from the URL. */ useEffect( () => { if ( ! hasSetFilterDefaultsFromUrl ) { @@ -168,7 +158,7 @@ const RatingFilterBlock = ( { initialFilters, ] ); - const onClick = ( clickedValue ) => () => { + const onClick = ( clickedValue: string ) => () => { if ( ! productRatingsArray.length ) { setProductRatings( [ clickedValue ] ); } else { @@ -185,7 +175,10 @@ const RatingFilterBlock = ( { } }; - if ( filteredCounts.rating_counts != undefined ) { + if ( + ! filteredCountsLoading && + filteredCounts.rating_counts !== undefined + ) { const orderedRatings = [ ...filteredCounts.rating_counts ].reverse(); return ( <> @@ -201,26 +194,18 @@ const RatingFilterBlock = ( { item.rating.toString() ) ? 'is-active' - : null + : '' } key={ item.rating } rating={ item.rating } ratedProductsCount={ item.count } onClick={ onClick( item.rating.toString() ) } - options={ displayedOptions } /> ) ) } - { blockAttributes.showFilterButton && ( - onSubmit( checked ) } - /> - ) } ); } - return ; + return null; }; export default RatingFilterBlock; diff --git a/assets/js/blocks/rating-filter/edit.tsx b/assets/js/blocks/rating-filter/edit.tsx index 6714fc3da7b..a63b990fc1e 100644 --- a/assets/js/blocks/rating-filter/edit.tsx +++ b/assets/js/blocks/rating-filter/edit.tsx @@ -1,16 +1,9 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; import classnames from 'classnames'; -import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; -import { - Disabled, - PanelBody, - ToggleControl, - withSpokenMessages, -} from '@wordpress/components'; -import HeadingToolbar from '@woocommerce/editor-components/heading-toolbar'; +import { useBlockProps } from '@wordpress/block-editor'; +import { Disabled, withSpokenMessages } from '@wordpress/components'; import BlockTitle from '@woocommerce/editor-components/block-title'; import type { BlockEditProps } from '@wordpress/blocks'; @@ -18,60 +11,24 @@ import type { BlockEditProps } from '@wordpress/blocks'; * Internal dependencies */ import Block from './block'; -// import './editor.scss'; import { Attributes } from './types'; const Edit = ( { attributes, setAttributes, }: BlockEditProps< Attributes > ) => { - const { className, heading, headingLevel, showCounts } = attributes; + const { className, heading, headingLevel } = attributes; const blockProps = useBlockProps( { - className: classnames( 'wc-block-stock-filter', className ), + className: classnames( 'wc-block-rating-filter', className ), } ); - const getInspectorControls = () => { - return ( - - - - setAttributes( { - showCounts: ! showCounts, - } ) - } - /> - - - ); - }; - return ( <> - { getInspectorControls() } {
diff --git a/assets/js/blocks/rating-filter/frontend.ts b/assets/js/blocks/rating-filter/frontend.ts index 445628e40ca..420a132834d 100644 --- a/assets/js/blocks/rating-filter/frontend.ts +++ b/assets/js/blocks/rating-filter/frontend.ts @@ -13,12 +13,10 @@ import { blockAttributes } from './attributes'; const getProps = ( el: HTMLElement ) => { return { attributes: { - showCounts: el.dataset.showCounts === 'true', heading: el.dataset.heading || blockAttributes.heading.default, headingLevel: el.dataset.headingLevel ? parseInt( el.dataset.headingLevel, 10 ) : metadata.attributes.headingLevel.default, - showFilterButton: el.dataset.showFilterButton === 'true', }, isEditor: false, }; diff --git a/assets/js/blocks/rating-filter/index.tsx b/assets/js/blocks/rating-filter/index.tsx index f2211fce120..8fc40a30d7a 100644 --- a/assets/js/blocks/rating-filter/index.tsx +++ b/assets/js/blocks/rating-filter/index.tsx @@ -36,21 +36,11 @@ registerBlockType( metadata, { edit, // Save the props to post content. save( { attributes }: { attributes: Attributes } ) { - const { - className, - showCounts, - heading, - headingLevel, - showFilterButton, - } = attributes; + const { className, heading, headingLevel } = attributes; const data: Record< string, unknown > = { - 'data-show-counts': showCounts, 'data-heading': heading, 'data-heading-level': headingLevel, }; - if ( showFilterButton ) { - data[ 'data-show-filter-button' ] = showFilterButton; - } return (
Date: Thu, 8 Sep 2022 00:32:36 +0200 Subject: [PATCH 05/10] Add an experimental build gate and update block title and description --- assets/js/blocks/rating-filter/block.json | 4 +- assets/js/blocks/rating-filter/index.tsx | 81 ++++++++++++----------- src/BlockTypesController.php | 2 +- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/assets/js/blocks/rating-filter/block.json b/assets/js/blocks/rating-filter/block.json index 1bc39733306..abeed83f211 100644 --- a/assets/js/blocks/rating-filter/block.json +++ b/assets/js/blocks/rating-filter/block.json @@ -1,8 +1,8 @@ { "name": "woocommerce/rating-filter", "version": "1.0.0", - "title": "Filter Products by Rating", - "description": "Allow customers to filter the grid by products rating. Works in combination with the All Products block.", + "title": "Filter by Rating", + "description": "Enable customers to filter the product grid by rating.", "category": "woocommerce", "keywords": [ "WooCommerce" ], "supports": { diff --git a/assets/js/blocks/rating-filter/index.tsx b/assets/js/blocks/rating-filter/index.tsx index 8fc40a30d7a..0d8afaa8be7 100644 --- a/assets/js/blocks/rating-filter/index.tsx +++ b/assets/js/blocks/rating-filter/index.tsx @@ -1,6 +1,7 @@ /** * External dependencies */ +import { isExperimentalBuild } from '@woocommerce/block-settings'; import { __ } from '@wordpress/i18n'; import { registerBlockType } from '@wordpress/blocks'; import { Icon, starEmpty } from '@wordpress/icons'; @@ -15,44 +16,46 @@ import metadata from './block.json'; import { blockAttributes } from './attributes'; import type { Attributes } from './types'; -registerBlockType( metadata, { - title: __( 'Filter Products by Rating', 'woo-gutenberg-products-block' ), - description: __( - 'Allow customers to filter the grid by products rating. Works in combination with the All Products block.', - 'woo-gutenberg-products-block' - ), - icon: { - src: ( - +if ( isExperimentalBuild() ) { + registerBlockType( metadata, { + title: __( 'Filter by Rating', 'woo-gutenberg-products-block' ), + description: __( + 'Enable customers to filter the product grid by rating.', + 'woo-gutenberg-products-block' ), - }, - attributes: { - ...metadata.attributes, - ...blockAttributes, - }, - edit, - // Save the props to post content. - save( { attributes }: { attributes: Attributes } ) { - const { className, heading, headingLevel } = attributes; - const data: Record< string, unknown > = { - 'data-heading': heading, - 'data-heading-level': headingLevel, - }; - return ( -
- -
- ); - }, -} ); + ), + }, + attributes: { + ...metadata.attributes, + ...blockAttributes, + }, + edit, + // Save the props to post content. + save( { attributes }: { attributes: Attributes } ) { + const { className, heading, headingLevel } = attributes; + const data: Record< string, unknown > = { + 'data-heading': heading, + 'data-heading-level': headingLevel, + }; + return ( +
+ +
+ ); + }, + } ); +} diff --git a/src/BlockTypesController.php b/src/BlockTypesController.php index 63c6d755036..e2f76b4408e 100644 --- a/src/BlockTypesController.php +++ b/src/BlockTypesController.php @@ -174,7 +174,6 @@ protected function get_block_types() { 'ProductOnSale', 'ProductsByAttribute', 'ProductTopRated', - 'RatingFilter', 'ReviewsByProduct', 'ReviewsByCategory', 'ProductSearch', @@ -183,6 +182,7 @@ protected function get_block_types() { 'PriceFilter', 'AttributeFilter', 'StockFilter', + 'RatingFilter', 'ActiveFilters', 'ClassicTemplate', 'ProductAddToCart', From 7d6fa6d4d31b3608760cb19e26d775a4b2533705 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Thu, 8 Sep 2022 01:31:06 +0200 Subject: [PATCH 06/10] Remove redundant title and description --- assets/js/blocks/rating-filter/index.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/assets/js/blocks/rating-filter/index.tsx b/assets/js/blocks/rating-filter/index.tsx index 0d8afaa8be7..386a2f381d0 100644 --- a/assets/js/blocks/rating-filter/index.tsx +++ b/assets/js/blocks/rating-filter/index.tsx @@ -2,7 +2,6 @@ * External dependencies */ import { isExperimentalBuild } from '@woocommerce/block-settings'; -import { __ } from '@wordpress/i18n'; import { registerBlockType } from '@wordpress/blocks'; import { Icon, starEmpty } from '@wordpress/icons'; import classNames from 'classnames'; @@ -18,11 +17,6 @@ import type { Attributes } from './types'; if ( isExperimentalBuild() ) { registerBlockType( metadata, { - title: __( 'Filter by Rating', 'woo-gutenberg-products-block' ), - description: __( - 'Enable customers to filter the product grid by rating.', - 'woo-gutenberg-products-block' - ), icon: { src: ( Date: Fri, 16 Sep 2022 14:34:29 +0200 Subject: [PATCH 07/10] Add support for the CheckboxList component in the Products by Rating block --- .../base/components/product-rating/index.tsx | 10 +- assets/js/blocks/rating-filter/block.tsx | 97 ++++++++++++------- 2 files changed, 65 insertions(+), 42 deletions(-) diff --git a/assets/js/base/components/product-rating/index.tsx b/assets/js/base/components/product-rating/index.tsx index 07225d75082..ca0f8347094 100644 --- a/assets/js/base/components/product-rating/index.tsx +++ b/assets/js/base/components/product-rating/index.tsx @@ -14,7 +14,6 @@ const Rating = ( { key, rating, ratedProductsCount, - onClick, }: RatingProps ): JSX.Element => { const ratingClassName = classNames( 'wc-block-components-product-rating', @@ -45,13 +44,7 @@ const Rating = ( { }; return ( -
+
void; } export default Rating; diff --git a/assets/js/blocks/rating-filter/block.tsx b/assets/js/blocks/rating-filter/block.tsx index 6f1ea970fdd..acd8f87880f 100644 --- a/assets/js/blocks/rating-filter/block.tsx +++ b/assets/js/blocks/rating-filter/block.tsx @@ -12,6 +12,7 @@ import { getSettingWithCoercion } from '@woocommerce/settings'; import { isBoolean } from '@woocommerce/types'; import isShallowEqual from '@wordpress/is-shallow-equal'; import { useState, useCallback, useMemo, useEffect } from '@wordpress/element'; +import CheckboxList from '@woocommerce/base-components/checkbox-list'; import { addQueryArgs, removeQueryArgs } from '@wordpress/url'; import { changeUrl } from '@woocommerce/utils'; @@ -46,9 +47,6 @@ const RatingFilterBlock = ( { const [ hasSetFilterDefaultsFromUrl, setHasSetFilterDefaultsFromUrl ] = useState( false ); - const TagName = - `h${ blockAttributes.headingLevel }` as keyof JSX.IntrinsicElements; - const [ queryState ] = useQueryStateByContext(); const { results: filteredCounts, isLoading: filteredCountsLoading } = @@ -57,6 +55,11 @@ const RatingFilterBlock = ( { queryState, } ); + const TagName = + `h${ blockAttributes.headingLevel }` as keyof JSX.IntrinsicElements; + const isLoading = ! blockAttributes.isPreview && filteredCountsLoading; + const isDisabled = ! blockAttributes.isPreview && ! filteredCountsLoading; + const initialFilters = useMemo( () => getActiveFilters( 'rating_filter' ), [] @@ -133,7 +136,7 @@ const RatingFilterBlock = ( { const currentClickedQuery = useShallowEqual( clickedQuery ); const previousClickedQuery = usePrevious( currentClickedQuery ); - // Track Stock query changes so the block reflects current filters. + // Track Rating query changes so the block reflects current filters. useEffect( () => { if ( ! isShallowEqual( previousClickedQuery, currentClickedQuery ) && // Clicked query changed. @@ -158,28 +161,59 @@ const RatingFilterBlock = ( { initialFilters, ] ); - const onClick = ( clickedValue: string ) => () => { - if ( ! productRatingsArray.length ) { - setProductRatings( [ clickedValue ] ); - } else { - const previouslyClicked = - productRatingsArray.includes( clickedValue ); - const newClicked = productRatingsArray.filter( - ( value ) => value !== clickedValue - ); - if ( ! previouslyClicked ) { - newClicked.push( clickedValue ); - newClicked.sort(); + /** + * When a checkbox in the list changes, update state. + */ + const onClick = useCallback( + ( clickedValue: string ) => { + if ( ! productRatingsArray.length ) { + setProductRatings( [ clickedValue ] ); + } else { + const previouslyClicked = + productRatingsArray.includes( clickedValue ); + const newClicked = productRatingsArray.filter( + ( value ) => value !== clickedValue + ); + if ( ! previouslyClicked ) { + newClicked.push( clickedValue ); + newClicked.sort(); + } + setProductRatings( newClicked ); } - setProductRatings( newClicked ); - } - }; + }, + [ productRatingsArray, setProductRatings ] + ); if ( ! filteredCountsLoading && filteredCounts.rating_counts !== undefined ) { const orderedRatings = [ ...filteredCounts.rating_counts ].reverse(); + + const displayedOptions = orderedRatings.map( ( item ) => { + if ( Object.keys( item ).length > 0 ) { + return { + value: item?.rating?.toString(), + name: 'Rating', + label: ( + + ), + }; + } + return null; + } ); + return ( <> { ! isEditor && blockAttributes.heading && ( @@ -187,21 +221,18 @@ const RatingFilterBlock = ( { { blockAttributes.heading } ) } - { orderedRatings.map( ( item ) => ( - + { + onClick( checked.toString() ); + } } + isLoading={ isLoading } + isDisabled={ isDisabled } /> - ) ) } +
); } From 73df337b0854806cbbed043168220078834c74e5 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Tue, 20 Sep 2022 09:07:48 +0200 Subject: [PATCH 08/10] Products by Rating: Minor code clean-up --- .../js/base/components/product-rating/index.tsx | 13 ++++--------- .../base/components/product-rating/style.scss | 2 -- assets/js/blocks/rating-filter/style.scss | 17 ++++++++++------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/assets/js/base/components/product-rating/index.tsx b/assets/js/base/components/product-rating/index.tsx index ca0f8347094..13f52da71a9 100644 --- a/assets/js/base/components/product-rating/index.tsx +++ b/assets/js/base/components/product-rating/index.tsx @@ -2,7 +2,7 @@ * External dependencies */ import classNames from 'classnames'; -import { __, _n, sprintf } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies @@ -32,19 +32,14 @@ const Rating = ( { const ratingHTML = { __html: sprintf( - /* translators: %1$s is referring to the rating value */ - _n( - 'Rated %1$s out of 5', - 'Rated %1$s out of 5', - ratedProductsCount, - 'woo-gutenberg-products-block' - ), + /* translators: %f is referring to the rating value */ + __( 'Rated %f out of 5', 'woo-gutenberg-products-block' ), sprintf( '%f', rating ) ), }; return ( -
+
Date: Tue, 20 Sep 2022 15:58:56 +0200 Subject: [PATCH 09/10] Active Filters: Fix the Clear All button for Ratings. Closes ##7172 --- assets/js/blocks/active-filters/block.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/js/blocks/active-filters/block.tsx b/assets/js/blocks/active-filters/block.tsx index c00ac6f7549..fc034b8f0b7 100644 --- a/assets/js/blocks/active-filters/block.tsx +++ b/assets/js/blocks/active-filters/block.tsx @@ -295,6 +295,7 @@ const ActiveFiltersBlock = ( { setMaxPrice( undefined ); setProductAttributes( [] ); setProductStockStatus( [] ); + setProductRatings( [] ); } } } > From 72654a48c6eb7dafa16dcd49d48ddaddccd1a6f0 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Thu, 22 Sep 2022 03:49:12 +0200 Subject: [PATCH 10/10] Products by Rating: Add misc TS fixes --- assets/js/blocks/rating-filter/block.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/assets/js/blocks/rating-filter/block.tsx b/assets/js/blocks/rating-filter/block.tsx index acd8f87880f..e90e97d22dd 100644 --- a/assets/js/blocks/rating-filter/block.tsx +++ b/assets/js/blocks/rating-filter/block.tsx @@ -9,7 +9,7 @@ import { useCollectionData, } from '@woocommerce/base-context/hooks'; import { getSettingWithCoercion } from '@woocommerce/settings'; -import { isBoolean } from '@woocommerce/types'; +import { isBoolean, isObject, objectHasProp } from '@woocommerce/types'; import isShallowEqual from '@wordpress/is-shallow-equal'; import { useState, useCallback, useMemo, useEffect } from '@wordpress/element'; import CheckboxList from '@woocommerce/base-components/checkbox-list'; @@ -186,15 +186,17 @@ const RatingFilterBlock = ( { if ( ! filteredCountsLoading && - filteredCounts.rating_counts !== undefined + objectHasProp( filteredCounts, 'rating_counts' ) && + Array.isArray( filteredCounts.rating_counts ) ) { const orderedRatings = [ ...filteredCounts.rating_counts ].reverse(); - const displayedOptions = orderedRatings.map( ( item ) => { - if ( Object.keys( item ).length > 0 ) { + const displayedOptions = orderedRatings + .filter( + ( item ) => isObject( item ) && Object.keys( item ).length > 0 + ) + .map( ( item ) => { return { - value: item?.rating?.toString(), - name: 'Rating', label: ( ), + value: item?.rating?.toString(), }; - } - return null; - } ); + } ); return ( <>