From 534465c9b0ee274303be71b25a4fb8767f1f3c46 Mon Sep 17 00:00:00 2001 From: Lucio Giannotta Date: Wed, 25 Jan 2023 22:33:44 +0100 Subject: [PATCH 01/25] Remove feature flag from Element variations Removes feature flag from: * Product Summary * Product Template * Product Title --- .../variations/elements/product-summary.tsx | 15 ++++++------- .../variations/elements/product-template.tsx | 21 ++++++++----------- .../variations/elements/product-title.tsx | 15 ++++++------- 3 files changed, 21 insertions(+), 30 deletions(-) diff --git a/assets/js/blocks/product-query/variations/elements/product-summary.tsx b/assets/js/blocks/product-query/variations/elements/product-summary.tsx index 69e0f33f775..6103789356f 100644 --- a/assets/js/blocks/product-query/variations/elements/product-summary.tsx +++ b/assets/js/blocks/product-query/variations/elements/product-summary.tsx @@ -1,7 +1,6 @@ /** * External dependencies */ -import { isFeaturePluginBuild } from '@woocommerce/block-settings'; import { Icon } from '@wordpress/components'; import { BLOCK_DESCRIPTION, @@ -17,11 +16,9 @@ import { registerElementVariation } from './utils'; export const CORE_NAME = 'core/post-excerpt'; export const VARIATION_NAME = 'woocommerce/product-query/product-summary'; -if ( isFeaturePluginBuild() ) { - registerElementVariation( CORE_NAME, { - blockDescription: BLOCK_DESCRIPTION, - blockIcon: , - blockTitle: BLOCK_TITLE, - variationName: VARIATION_NAME, - } ); -} +registerElementVariation( CORE_NAME, { + blockDescription: BLOCK_DESCRIPTION, + blockIcon: , + blockTitle: BLOCK_TITLE, + variationName: VARIATION_NAME, +} ); diff --git a/assets/js/blocks/product-query/variations/elements/product-template.tsx b/assets/js/blocks/product-query/variations/elements/product-template.tsx index 60eead256f5..e234c006a9f 100644 --- a/assets/js/blocks/product-query/variations/elements/product-template.tsx +++ b/assets/js/blocks/product-query/variations/elements/product-template.tsx @@ -1,7 +1,6 @@ /** * External dependencies */ -import { isFeaturePluginBuild } from '@woocommerce/block-settings'; import { Icon } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { layout } from '@wordpress/icons'; @@ -14,14 +13,12 @@ import { registerElementVariation } from './utils'; export const CORE_NAME = 'core/post-template'; export const VARIATION_NAME = 'woocommerce/product-query/product-template'; -if ( isFeaturePluginBuild() ) { - registerElementVariation( CORE_NAME, { - blockDescription: __( - 'Contains the block elements used to render a product, like its name, featured image, rating, and more.', - 'woo-gutenberg-products-block' - ), - blockIcon: , - blockTitle: __( 'Product template', 'woo-gutenberg-products-block' ), - variationName: VARIATION_NAME, - } ); -} +registerElementVariation( CORE_NAME, { + blockDescription: __( + 'Contains the block elements used to render a product, like its name, featured image, rating, and more.', + 'woo-gutenberg-products-block' + ), + blockIcon: , + blockTitle: __( 'Product template', 'woo-gutenberg-products-block' ), + variationName: VARIATION_NAME, +} ); diff --git a/assets/js/blocks/product-query/variations/elements/product-title.tsx b/assets/js/blocks/product-query/variations/elements/product-title.tsx index 98e16e4e725..40bab90a607 100644 --- a/assets/js/blocks/product-query/variations/elements/product-title.tsx +++ b/assets/js/blocks/product-query/variations/elements/product-title.tsx @@ -1,7 +1,6 @@ /** * External dependencies */ -import { isFeaturePluginBuild } from '@woocommerce/block-settings'; import { Icon } from '@wordpress/components'; import { BLOCK_DESCRIPTION, @@ -17,11 +16,9 @@ import { registerElementVariation } from './utils'; export const CORE_NAME = 'core/post-title'; export const VARIATION_NAME = 'woocommerce/product-query/product-title'; -if ( isFeaturePluginBuild() ) { - registerElementVariation( CORE_NAME, { - blockDescription: BLOCK_DESCRIPTION, - blockIcon: , - blockTitle: BLOCK_TITLE, - variationName: VARIATION_NAME, - } ); -} +registerElementVariation( CORE_NAME, { + blockDescription: BLOCK_DESCRIPTION, + blockIcon: , + blockTitle: BLOCK_TITLE, + variationName: VARIATION_NAME, +} ); From eed60fbcd172adc1ac3ceb9fc490f64cebb5e224 Mon Sep 17 00:00:00 2001 From: Lucio Giannotta Date: Wed, 22 Feb 2023 20:42:46 +0100 Subject: [PATCH 02/25] Refactor `useProductAttributes` hook * Move it into the shared hooks. * Fetch both terms AND attributes via the API (previously, we got the attributes from the settings, but we'd get partial objects compared to the API? Maybe a follow-up to this could be to check why the attributes stored in the settings are incomplete?) * Make sure the return values match the ones expected from search items. --- assets/js/base/context/hooks/tsconfig.json | 3 +- .../context/hooks/use-product-attributes.ts | 63 +++++++++++++++++++ assets/js/types/type-defs/attributes.ts | 16 ++++- 3 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 assets/js/base/context/hooks/use-product-attributes.ts diff --git a/assets/js/base/context/hooks/tsconfig.json b/assets/js/base/context/hooks/tsconfig.json index da9dfb458fc..1c84e4557f6 100644 --- a/assets/js/base/context/hooks/tsconfig.json +++ b/assets/js/base/context/hooks/tsconfig.json @@ -6,7 +6,8 @@ "../../../../../packages/checkout/index.js", "../providers/cart-checkout/checkout-events/index.tsx", "../providers/cart-checkout/payment-events/index.tsx", - "../providers/cart-checkout/shipping/index.js" + "../providers/cart-checkout/shipping/index.js", + "../../../editor-components/utils/*" ], "exclude": [ "**/test/**" ] } diff --git a/assets/js/base/context/hooks/use-product-attributes.ts b/assets/js/base/context/hooks/use-product-attributes.ts new file mode 100644 index 00000000000..b9520f0b328 --- /dev/null +++ b/assets/js/base/context/hooks/use-product-attributes.ts @@ -0,0 +1,63 @@ +/** + * External dependencies + */ +import { useEffect, useRef, useState } from '@wordpress/element'; +import { getAttributes, getTerms } from '@woocommerce/editor-components/utils'; +import { + AttributeObject, + AttributeTerm, + AttributeWithTerms, +} from '@woocommerce/types'; + +export default function useProductAttributes( shouldLoadAttributes: boolean ) { + const [ isLoadingAttributes, setIsLoadingAttributes ] = useState( false ); + const [ productsAttributes, setProductsAttributes ] = useState< + AttributeWithTerms[] + >( [] ); + const hasLoadedAttributes = useRef( false ); + + useEffect( () => { + if ( + ! shouldLoadAttributes || + isLoadingAttributes || + hasLoadedAttributes.current + ) + return; + + async function fetchAttributesWithTerms() { + setIsLoadingAttributes( true ); + + const attributes: AttributeObject[] = await getAttributes(); + const attributesWithTerms: AttributeWithTerms[] = []; + + for ( const attribute of attributes ) { + const terms: AttributeTerm[] = await getTerms( attribute.id ); + + attributesWithTerms.push( { + ...attribute, + parent: 0, + // Manually adding the parent id because of a Rest API bug + // returning always `0` as parent. + // see https://github.com/woocommerce/woocommerce-blocks/issues/8501 + terms: terms.map( ( term ) => ( { + ...term, + attr_slug: attribute.taxonomy, + parent: attribute.id, + } ) ), + } ); + } + + setProductsAttributes( attributesWithTerms ); + hasLoadedAttributes.current = true; + setIsLoadingAttributes( false ); + } + + fetchAttributesWithTerms(); + + return () => { + hasLoadedAttributes.current = true; + }; + }, [ isLoadingAttributes, shouldLoadAttributes ] ); + + return { isLoadingAttributes, productsAttributes }; +} diff --git a/assets/js/types/type-defs/attributes.ts b/assets/js/types/type-defs/attributes.ts index 925eb9a429d..13be23a692d 100644 --- a/assets/js/types/type-defs/attributes.ts +++ b/assets/js/types/type-defs/attributes.ts @@ -2,16 +2,21 @@ export interface AttributeSetting { attribute_id: string; attribute_name: string; attribute_label: string; - attribute_type: string; attribute_orderby: 'menu_order' | 'name' | 'name_num' | 'id'; attribute_public: 0 | 1; + attribute_type: string; } export interface AttributeObject { + count: number; + has_archives: boolean; id: number; + label: string; name: string; + order: 'menu_order' | 'name' | 'name_num' | 'id'; + parent: number; taxonomy: string; - label: string; + type: string; } export interface AttributeQuery { @@ -28,3 +33,10 @@ export interface AttributeTerm { parent: number; slug: string; } + +export interface AttributeMetadata { + taxonomy: string; + termId: number; +} + +export type AttributeWithTerms = AttributeObject & { terms: AttributeTerm[] }; From eb5e92c12631d6680571e717a5614dcd15ce3b4a Mon Sep 17 00:00:00 2001 From: Lucio Giannotta Date: Wed, 22 Feb 2023 20:43:15 +0100 Subject: [PATCH 03/25] Remove attribute-related types from PQ directory --- assets/js/blocks/product-query/types.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/assets/js/blocks/product-query/types.ts b/assets/js/blocks/product-query/types.ts index e8bc6128918..aaff3794417 100644 --- a/assets/js/blocks/product-query/types.ts +++ b/assets/js/blocks/product-query/types.ts @@ -1,18 +1,7 @@ /** * External dependencies */ -import type { - AttributeSetting, - AttributeTerm, - EditorBlock, -} from '@woocommerce/types'; - -export interface AttributeMetadata { - taxonomy: string; - termId: number; -} - -export type AttributeWithTerms = AttributeSetting & { terms: AttributeTerm[] }; +import type { EditorBlock } from '@woocommerce/types'; // The interface below disables the forbidden underscores // naming convention because we are namespacing our From 14e9373d0b0433353429bf3c4d8ccbb660543778 Mon Sep 17 00:00:00 2001 From: Lucio Giannotta Date: Wed, 22 Feb 2023 20:47:12 +0100 Subject: [PATCH 04/25] Improve functionality of `SearchListControl` * Allow the search input to be a Token based input. * Allow for search input to search even collapsed properties. * Use core `CheckboxControl` instead of radio buttons for items having children (includes undeterminated state). Please enter the commit message for your changes. Lines starting --- .../inspector-controls/attributes-filter.tsx | 83 +++++++------------ .../expandable-search-list-item.tsx | 15 ++-- .../{index.js => index.tsx} | 49 +++++++---- .../search-list-control/item.tsx | 41 ++++++++- .../search-list-control.tsx | 73 ++++++++++++---- .../search-list-control/types.ts | 12 ++- 6 files changed, 172 insertions(+), 101 deletions(-) rename assets/js/editor-components/product-attribute-term-control/{index.js => index.tsx} (83%) diff --git a/assets/js/blocks/product-query/inspector-controls/attributes-filter.tsx b/assets/js/blocks/product-query/inspector-controls/attributes-filter.tsx index 2ef1b645465..c73e56d7bd7 100644 --- a/assets/js/blocks/product-query/inspector-controls/attributes-filter.tsx +++ b/assets/js/blocks/product-query/inspector-controls/attributes-filter.tsx @@ -7,6 +7,7 @@ import { // eslint-disable-next-line @wordpress/no-unsafe-wp-apis __experimentalToolsPanelItem as ToolsPanelItem, } from '@wordpress/components'; +import { useEffect, useState } from '@wordpress/element'; /** * Internal dependencies @@ -18,6 +19,7 @@ import { } from '../types'; import useProductAttributes from '../useProductAttributes'; import { setQueryAttribute } from '../utils'; +import ProductAttributeTermControl from '@woocommerce/editor-components/product-attribute-term-control'; function getAttributeMetadataFromToken( token: string, @@ -88,68 +90,39 @@ function getInputValueFromQueryParam( export const AttributesFilter = ( props: ProductQueryBlock ) => { const { query } = props.attributes; - const { isLoadingAttributes, productsAttributes } = - useProductAttributes( true ); - - const attributesSuggestions = productsAttributes.reduce( ( acc, curr ) => { - const namespacedTerms = curr.terms.map( - ( term ) => `${ curr.attribute_label }: ${ term.name }` - ); - - return [ ...acc, ...namespacedTerms ]; - }, [] as string[] ); + const [ selected, setSelected ] = useState< { id: number }[] >( [] ); + + useEffect( () => { + if ( query.__woocommerceAttributes?.length ) { + setSelected( + query.__woocommerceAttributes.map( ( { termId: id } ) => ( { + id, + } ) ) + ); + } + }, [ query.__woocommerceAttributes ] ); return ( query.__woocommerceAttributes?.length } > - { - let __woocommerceAttributes; - - try { - __woocommerceAttributes = attributes.map( - ( attribute ) => { - attribute = - typeof attribute === 'string' - ? attribute - : attribute.value; - - return getAttributeMetadataFromToken( - attribute, - productsAttributes - ); - } - ); - - setQueryAttribute( props, { - __woocommerceAttributes, - } ); - } catch ( ok ) { - // Not required to do anything here - // Input validation is handled by the `validateInput` - // below, and we don't need to save anything. - } + { + const __woocommerceAttributes = value.map( + ( { id, attr_slug } ) => ( { + termId: id, + taxonomy: attr_slug, + } ) + ); + + setQueryAttribute( props, { + __woocommerceAttributes, + } ); } } - suggestions={ attributesSuggestions } - validateInput={ ( value: string ) => - attributesSuggestions.includes( value ) - } - value={ - isLoadingAttributes - ? [ __( 'Loading…', 'woo-gutenberg-products-block' ) ] - : getInputValueFromQueryParam( - query.__woocommerceAttributes, - productsAttributes - ) - } - __experimentalExpandOnFocus={ true } + operator={ 'any' } + isCompact={ true } /> ); diff --git a/assets/js/editor-components/expandable-search-list-item/expandable-search-list-item.tsx b/assets/js/editor-components/expandable-search-list-item/expandable-search-list-item.tsx index 79f71c61e2c..016f36a0651 100644 --- a/assets/js/editor-components/expandable-search-list-item/expandable-search-list-item.tsx +++ b/assets/js/editor-components/expandable-search-list-item/expandable-search-list-item.tsx @@ -2,20 +2,15 @@ * External dependencies */ import { SearchListItem } from '@woocommerce/editor-components/search-list-control'; +import { + renderItemArgs, + SearchListItemType, +} from '@woocommerce/editor-components/search-list-control/types'; import { Spinner } from '@wordpress/components'; import classNames from 'classnames'; -interface SearchListItem { - id: string; -} - -interface ExpandableSearchListItemProps { - className?: string; - item: SearchListItem; - isSelected: boolean; +interface ExpandableSearchListItemProps extends renderItemArgs { isLoading: boolean; - onSelect: () => void; - disabled: boolean; } const ExpandableSearchListItem = ( { diff --git a/assets/js/editor-components/product-attribute-term-control/index.js b/assets/js/editor-components/product-attribute-term-control/index.tsx similarity index 83% rename from assets/js/editor-components/product-attribute-term-control/index.js rename to assets/js/editor-components/product-attribute-term-control/index.tsx index fae36c0db18..af842f5e398 100644 --- a/assets/js/editor-components/product-attribute-term-control/index.js +++ b/assets/js/editor-components/product-attribute-term-control/index.tsx @@ -1,18 +1,29 @@ /** * External dependencies */ -import { __, _n, sprintf } from '@wordpress/i18n'; +import classNames from 'classnames'; import PropTypes from 'prop-types'; +import { useEffect, useState } from '@wordpress/element'; +import { __, _n, sprintf } from '@wordpress/i18n'; import { SearchListControl, SearchListItem, } from '@woocommerce/editor-components/search-list-control'; import { SelectControl } from '@wordpress/components'; import { withInstanceId } from '@wordpress/compose'; +import useProductAttributes from '@woocommerce/base-context/hooks/use-product-attributes'; import { withAttributes } from '@woocommerce/block-hocs'; import ErrorMessage from '@woocommerce/editor-components/error-placeholder/error-message'; -import classNames from 'classnames'; -import ExpandableSearchListItem from '@woocommerce/editor-components/expandable-search-list-item/expandable-search-list-item.tsx'; +import ExpandableSearchListItem from '@woocommerce/editor-components/expandable-search-list-item/expandable-search-list-item'; +import { + AttributeObject, + AttributeTerm, + AttributeWithTerms, +} from '@woocommerce/types'; +import { + renderItemArgs, + SearchListItemType, +} from '@woocommerce/editor-components/search-list-control/types'; /** * Internal dependencies @@ -23,6 +34,7 @@ const ProductAttributeTermControl = ( { attributes, error, expandedAttribute, + expandedItemId, onChange, onExpandAttribute, onOperatorChange, @@ -34,7 +46,10 @@ const ProductAttributeTermControl = ( { termsAreLoading, termsList, } ) => { - const renderItem = ( args ) => { + const { isLoadingAttributes, productsAttributes } = + useProductAttributes( true ); + + const renderItem = ( args: renderItemArgs ) => { const { item, search, depth = 0 } = args; const classes = [ 'woocommerce-product-attributes__item', @@ -46,17 +61,17 @@ const ProductAttributeTermControl = ( { ]; if ( ! item.breadcrumbs.length ) { - const isSelected = expandedAttribute === item.id; + const isExpanded = expandedItemId === item.id; return ( { return () => { onChange( [] ); @@ -121,8 +136,11 @@ const ProductAttributeTermControl = ( { ); }; - const currentTerms = termsList[ expandedAttribute ] || []; - const currentList = [ ...attributes, ...currentTerms ]; + const list = productsAttributes.reduce( ( acc, curr ) => { + const { terms, ...props } = curr; + + return [ ...acc, { ...props }, ...terms ]; + }, [] as Array< AttributeObject | AttributeTerm > ); const messages = { clear: __( @@ -138,7 +156,7 @@ const ProductAttributeTermControl = ( { 'Search for product attributes', 'woo-gutenberg-products-block' ), - selected: ( n ) => + selected: ( n: number ) => sprintf( /* translators: %d is the count of attributes selected. */ _n( @@ -163,13 +181,11 @@ const ProductAttributeTermControl = ( { <> - currentList.find( - ( currentListItem ) => currentListItem.id === id - ) + list.find( ( term ) => term.id === id ) ) .filter( Boolean ) } onChange={ onChange } @@ -177,6 +193,7 @@ const ProductAttributeTermControl = ( { messages={ messages } isCompact={ isCompact } isHierarchical + type={ 'token' } /> { !! onOperatorChange && (