diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/block.json index b8033c77cb3..6a497c20a33 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/block.json +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/block.json @@ -14,7 +14,45 @@ "inserter": false, "color": { "text": true, - "background": false + "background": false, + "__experimentalDefaultControls": { + "text": false + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontWeight": true, + "__experimentalFontFamily": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": false + } + }, + "spacing": { + "margin": true, + "padding": true, + "blockGap": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false, + "blockGap": false + } + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "color": false, + "radius": false, + "style": false, + "width": false + } } }, "usesContext": [ "query", "queryId" ], @@ -42,6 +80,18 @@ "isPreview": { "type": "boolean", "default": false + }, + "sortOrder": { + "type": "string", + "default": "count-desc" + }, + "hideEmpty": { + "type": "boolean", + "default": true + }, + "clearButton": { + "type": "boolean", + "default":true } } } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/components/inspector-controls.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/components/inspector-controls.tsx deleted file mode 100644 index 7dfd23c4663..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/components/inspector-controls.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/** - * External dependencies - */ -import { __, _x } from '@wordpress/i18n'; -import { InspectorControls } from '@wordpress/block-editor'; -import { - PanelBody, - ToggleControl, - // @ts-expect-error - no types. - // eslint-disable-next-line @wordpress/no-unsafe-wp-apis - __experimentalToggleGroupControl as ToggleGroupControl, - // @ts-expect-error - no types. - // eslint-disable-next-line @wordpress/no-unsafe-wp-apis - __experimentalToggleGroupControlOption as ToggleGroupControlOption, -} from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { AttributeSelectControls } from './attribute-select-controls'; -import { EditProps } from '../types'; - -export const Inspector = ( { attributes, setAttributes }: EditProps ) => { - const { attributeId, showCounts, queryType, displayStyle, selectType } = - attributes; - - return ( - - - - setAttributes( { - showCounts: ! showCounts, - } ) - } - /> - - setAttributes( { - selectType: value, - } ) - } - className="wc-block-attribute-filter__multiple-toggle" - > - - - - { selectType === 'multiple' && ( - - setAttributes( { - queryType: value, - } ) - } - className="wc-block-attribute-filter__conditions-toggle" - > - - - - ) } - - setAttributes( { - displayStyle: value, - } ) - } - className="wc-block-attribute-filter__display-toggle" - > - - - - - - { - setAttributes( { - attributeId: id, - } ); - } } - /> - - - ); -}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/components/inspector.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/components/inspector.tsx new file mode 100644 index 00000000000..77ab2844d2d --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/components/inspector.tsx @@ -0,0 +1,160 @@ +/** + * External dependencies + */ +import { getSetting } from '@woocommerce/settings'; +import { AttributeSetting } from '@woocommerce/types'; +import { InspectorControls } from '@wordpress/block-editor'; +import { createInterpolateElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { + ComboboxControl, + PanelBody, + SelectControl, + ToggleControl, + // @ts-expect-error - no types. + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalToggleGroupControl as ToggleGroupControl, + // @ts-expect-error - no types. + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalToggleGroupControlOption as ToggleGroupControlOption, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { sortOrderOptions } from '../constants'; +import { BlockAttributes, EditProps } from '../types'; + +const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] ); + +export const Inspector = ( { attributes, setAttributes }: EditProps ) => { + const { + attributeId, + sortOrder, + queryType, + displayStyle, + showCounts, + hideEmpty, + clearButton, + } = attributes; + + return ( + <> + + + ( { + value: item.attribute_id, + label: item.attribute_label, + } ) ) } + value={ attributeId + '' } + onChange={ ( value ) => + setAttributes( { + attributeId: parseInt( value || '', 10 ), + } ) + } + help={ __( + 'Choose the attribute to show in this filter.', + 'woocommerce' + ) } + /> + + + + setAttributes( { sortOrder: value } ) + } + help={ __( + 'Determine the order of filter options.', + 'woocommerce' + ) } + /> + + setAttributes( { queryType: value } ) + } + style={ { width: '100%' } } + help={ + queryType === 'and' + ? createInterpolateElement( + __( + 'Show results for all selected attributes. Displayed products must contain all of them to appear in the results.', + 'woocommerce' + ), + { + b: , + } + ) + : __( + 'Show results for any of the attributes selected (displayed products don’t have to have them all).', + 'woocommerce' + ) + } + > + + + + + + + + setAttributes( { displayStyle: value } ) } + style={ { width: '100%' } } + > + + + + + setAttributes( { showCounts: value } ) + } + /> + + setAttributes( { hideEmpty: ! value } ) + } + /> + + setAttributes( { clearButton: value } ) + } + /> + + + + ); +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/constants.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/constants.ts index f5114d59ea6..8c47d8c30e4 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/constants.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/constants.ts @@ -1,7 +1,12 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + export const attributeOptionsPreview = [ { id: 23, - name: 'Blue', + name: __( 'Blue', 'woocommerce' ), slug: 'blue', attr_slug: 'blue', description: '', @@ -10,7 +15,7 @@ export const attributeOptionsPreview = [ }, { id: 29, - name: 'Gray', + name: __( 'Gray', 'woocommerce' ), slug: 'gray', attr_slug: 'gray', description: '', @@ -19,7 +24,7 @@ export const attributeOptionsPreview = [ }, { id: 24, - name: 'Green', + name: __( 'Green', 'woocommerce' ), slug: 'green', attr_slug: 'green', description: '', @@ -28,7 +33,7 @@ export const attributeOptionsPreview = [ }, { id: 25, - name: 'Red', + name: __( 'Red', 'woocommerce' ), slug: 'red', attr_slug: 'red', description: '', @@ -37,7 +42,7 @@ export const attributeOptionsPreview = [ }, { id: 30, - name: 'Yellow', + name: __( 'Yellow', 'woocommerce' ), slug: 'yellow', attr_slug: 'yellow', description: '', @@ -45,3 +50,17 @@ export const attributeOptionsPreview = [ count: 1, }, ]; + +export const sortOrders = { + 'name-asc': __( 'Name, A to Z', 'woocommerce' ), + 'name-desc': __( 'Name, Z to A', 'woocommerce' ), + 'count-desc': __( 'Most results first', 'woocommerce' ), + 'count-asc': __( 'Least results first', 'woocommerce' ), +}; + +export const sortOrderOptions = Object.entries( sortOrders ).map( + ( [ value, label ] ) => ( { + label, + value, + } ) +); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/edit.tsx index 2b4b26ff388..d535cee6a20 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/edit.tsx @@ -2,8 +2,8 @@ * External dependencies */ import { __, sprintf } from '@wordpress/i18n'; -import { useCallback, useEffect, useState } from '@wordpress/element'; -import { BlockControls, useBlockProps } from '@wordpress/block-editor'; +import { useEffect, useState } from '@wordpress/element'; +import { useBlockProps } from '@wordpress/block-editor'; import { getSetting } from '@woocommerce/settings'; import { useCollection, @@ -14,26 +14,16 @@ import { AttributeTerm, objectHasProp, } from '@woocommerce/types'; -import { - Disabled, - Button, - ToolbarGroup, - withSpokenMessages, - Notice, -} from '@wordpress/components'; +import { Disabled, withSpokenMessages, Notice } from '@wordpress/components'; import { dispatch, useSelect } from '@wordpress/data'; /** * Internal dependencies */ import { EditProps, isAttributeCounts } from './types'; -import { - NoAttributesPlaceholder, - AttributesPlaceholder, -} from './components/placeholder'; -import { AttributeSelectControls } from './components/attribute-select-controls'; +import { NoAttributesPlaceholder } from './components/placeholder'; import { getAttributeFromId } from './utils'; -import { Inspector } from './components/inspector-controls'; +import { Inspector } from './components/inspector'; import { AttributeCheckboxList } from './components/attribute-checkbox-list'; import { AttributeDropdown } from './components/attribute-dropdown'; import { attributeOptionsPreview } from './constants'; @@ -41,84 +31,21 @@ import './style.scss'; const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] ); -const Toolbar = ( { - onClick, - isEditing, -}: { - onClick: () => void; - isEditing: boolean; -} ) => ( - - - -); - -const Wrapper = ( { - children, - onClickToolbarEdit, - isEditing, - blockProps, -}: { - children: React.ReactNode; - onClickToolbarEdit: () => void; - isEditing: boolean; - blockProps: object; -} ) => ( -
- - { children } -
-); - -const AttributeSelectPlaceholder = ( { - attributeId, - setAttributeId, - onClickDone, -}: { - attributeId: number; - setAttributeId: ( id: number ) => void; - onClickDone: () => void; -} ) => ( - -
- - -
-
-); - const Edit = ( props: EditProps ) => { - const { - attributes: blockAttributes, - setAttributes, - debouncedSpeak, - clientId, - } = props; + const { attributes: blockAttributes, clientId } = props; - const { attributeId, queryType, isPreview, displayStyle, showCounts } = - blockAttributes; + const { + attributeId, + queryType, + isPreview, + displayStyle, + showCounts, + sortOrder, + hideEmpty, + } = blockAttributes; const attributeObject = getAttributeFromId( attributeId ); - const [ isEditing, setIsEditing ] = useState( - ! attributeId && ! isPreview - ); - const [ attributeOptions, setAttributeOptions ] = useState< AttributeTerm[] >( [] ); @@ -130,7 +57,7 @@ const Edit = ( props: EditProps ) => { resourceName: 'products/attributes/terms', resourceValues: [ attributeObject?.id || 0 ], shouldSelect: blockAttributes.attributeId > 0, - query: { orderby: 'menu_order' }, + query: { orderby: 'menu_order', hide_empty: hideEmpty }, } ); const { results: filteredCounts } = useCollectionData( { @@ -183,8 +110,6 @@ const Edit = ( props: EditProps ) => { [ clientId ] ); - const blockProps = useBlockProps(); - useEffect( () => { const termIdHasProducts = objectHasProp( filteredCounts, 'attribute_counts' ) && @@ -192,14 +117,31 @@ const Edit = ( props: EditProps ) => { ? filteredCounts.attribute_counts.map( ( term ) => term.term ) : []; - if ( termIdHasProducts.length === 0 ) return setAttributeOptions( [] ); + if ( termIdHasProducts.length === 0 && hideEmpty ) + return setAttributeOptions( [] ); setAttributeOptions( - attributeTerms.filter( ( term ) => { - return termIdHasProducts.includes( term.id ); - } ) + attributeTerms + .filter( ( term ) => { + if ( hideEmpty ) + return termIdHasProducts.includes( term.id ); + return true; + } ) + .sort( ( a, b ) => { + switch ( sortOrder ) { + case 'name-asc': + return a.name > b.name ? 1 : -1; + case 'name-desc': + return a.name < b.name ? 1 : -1; + case 'count-asc': + return a.count > b.count ? 1 : -1; + case 'count-desc': + default: + return a.count < b.count ? 1 : -1; + } + } ) ); - }, [ attributeTerms, filteredCounts ] ); + }, [ attributeTerms, filteredCounts, sortOrder, hideEmpty ] ); useEffect( () => { if ( productFilterWrapperBlockId ) { @@ -231,36 +173,16 @@ const Edit = ( props: EditProps ) => { updateBlockAttributes, ] ); - const onClickDone = useCallback( () => { - setIsEditing( false ); - debouncedSpeak( - __( - 'Now displaying a preview of the Filter Products by Attribute block.', - 'woocommerce' - ) - ); - }, [ setIsEditing ] ); - - const setAttributeId = useCallback( - ( id ) => { - setAttributes( { - attributeId: id, - } ); - }, - [ setAttributes ] + const Wrapper = ( { children }: { children: React.ReactNode } ) => ( +
+ + { children } +
); - const toggleEditing = useCallback( () => { - setIsEditing( ! isEditing ); - }, [ isEditing ] ); - if ( isPreview ) { return ( - + { // Block rendering starts. if ( Object.keys( ATTRIBUTES ).length === 0 ) return ( - + ); - if ( isEditing ) - return ( - - - - ); - if ( ! attributeId || ! attributeObject ) return ( - +

{ __( @@ -318,11 +217,7 @@ const Edit = ( props: EditProps ) => { if ( attributeOptions.length === 0 ) return ( - +

{ __( @@ -335,12 +230,7 @@ const Edit = ( props: EditProps ) => { ); return ( - - + { displayStyle === 'dropdown' ? ( ( + 'defaultProductFilterAttribute' + ); + registerBlockType( metadata, { edit: Edit, icon: productFilterOptions, + attributes: { + ...metadata.attributes, + attributeId: { + ...metadata.attributes.attributeId, + default: parseInt( defaultAttribute.attribute_id, 10 ), + }, + }, } ); } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/types.ts index 440b5b0045b..30883d8a680 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/types.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/types.ts @@ -3,13 +3,21 @@ */ import { BlockEditProps } from '@wordpress/blocks'; +/** + * Internal dependencies + */ +import { sortOrders } from './constants'; + export type BlockAttributes = { attributeId: number; showCounts: boolean; - queryType: string; + queryType: 'or' | 'and'; displayStyle: string; selectType: string; isPreview: boolean; + sortOrder: keyof typeof sortOrders; + hideEmpty: boolean; + clearButton: boolean; }; export interface EditProps extends BlockEditProps< BlockAttributes > { diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/edit.tsx index e81fdbd8d2a..c8c8c697caf 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/edit.tsx @@ -1,34 +1,40 @@ /** * External dependencies */ -import { - InnerBlocks, - useBlockProps, - useInnerBlocksProps, - InspectorControls, -} from '@wordpress/block-editor'; -import { BlockEditProps, InnerBlockTemplate } from '@wordpress/blocks'; -import { __, sprintf } from '@wordpress/i18n'; -import { useCollection } from '@woocommerce/base-context/hooks'; -import { AttributeTerm } from '@woocommerce/types'; -import { - PanelBody, - RadioControl, - Spinner, - ExternalLink, - RangeControl, - __experimentalToggleGroupControlOption as ToggleGroupControlOption, - __experimentalToggleGroupControl as ToggleGroupControl, -} from '@wordpress/components'; -import { Icon, settings, menu } from '@wordpress/icons'; import { filter, filterThreeLines } from '@woocommerce/icons'; import { getSetting } from '@woocommerce/settings'; +import { AttributeSetting } from '@woocommerce/types'; +import { + InnerBlocks, + InspectorControls, + useBlockProps, + useInnerBlocksProps, +} from '@wordpress/block-editor'; +import { BlockEditProps, InnerBlockTemplate } from '@wordpress/blocks'; +import { __ } from '@wordpress/i18n'; +import { Icon, menu, settings } from '@wordpress/icons'; +import { + ExternalLink, + PanelBody, + RadioControl, + RangeControl, + // @ts-expect-error - no types. + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalToggleGroupControl as ToggleGroupControl, + // @ts-expect-error - no types. + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalToggleGroupControlOption as ToggleGroupControlOption, +} from '@wordpress/components'; /** * Internal dependencies */ -import type { BlockAttributes } from './types'; import './editor.scss'; +import type { BlockAttributes } from './types'; + +const defaultAttribute = getSetting< AttributeSetting >( + 'defaultProductFilterAttribute' +); const TEMPLATE: InnerBlockTemplate[] = [ [ @@ -64,8 +70,8 @@ const TEMPLATE: InnerBlockTemplate[] = [ 'woocommerce/product-filter', { filterType: 'attribute-filter', - heading: __( 'Attribute', 'woocommerce' ), - attributeId: 0, + heading: defaultAttribute.attribute_label, + attributeId: parseInt( defaultAttribute.attribute_id, 10 ), }, ], [ @@ -101,73 +107,11 @@ const TEMPLATE: InnerBlockTemplate[] = [ ], ]; -const addHighestProductCountAttributeToTemplate = ( - template: InnerBlockTemplate[], - highestProductCountAttribute: AttributeTerm | null -): InnerBlockTemplate[] => { - if ( highestProductCountAttribute === null ) return template; - - return template.map( ( block ) => { - const blockNameIndex = 0; - const blockAttributesIndex = 1; - const blockName = block[ blockNameIndex ]; - const blockAttributes = block[ blockAttributesIndex ]; - if ( - blockName === 'woocommerce/product-filter' && - blockAttributes?.filterType === 'attribute-filter' - ) { - return [ - blockName, - { - ...blockAttributes, - heading: highestProductCountAttribute.name, - attributeId: highestProductCountAttribute.id, - metadata: { - name: sprintf( - /* translators: %s is referring to the filter attribute name. For example: Color, Size, etc. */ - __( '%s (Experimental)', 'woocommerce' ), - highestProductCountAttribute.name - ), - }, - }, - ]; - } - - return block; - } ); -}; - export const Edit = ( { setAttributes, attributes, }: BlockEditProps< BlockAttributes > ) => { const blockProps = useBlockProps(); - const { results: attributeTerms, isLoading } = - useCollection< AttributeTerm >( { - namespace: '/wc/store/v1', - resourceName: 'products/attributes', - } ); - - const highestProductCountAttribute = - attributeTerms.reduce< AttributeTerm | null >( - ( attributeWithMostProducts, attribute ) => { - if ( attributeWithMostProducts === null ) { - return attribute; - } - return attribute.count > attributeWithMostProducts.count - ? attribute - : attributeWithMostProducts; - }, - null - ); - const updatedTemplate = addHighestProductCountAttributeToTemplate( - TEMPLATE, - highestProductCountAttribute - ); - - if ( isLoading ) { - return ; - } const templatePartEditUri = getSetting< string >( 'templatePartProductFiltersOverlayEditUri', @@ -329,7 +273,7 @@ export const Edit = ( { ) } - + ); }; diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter.block_theme.spec.ts new file mode 100644 index 00000000000..996af1d0774 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter.block_theme.spec.ts @@ -0,0 +1,154 @@ +/** + * External dependencies + */ +import { test as base, expect } from '@woocommerce/e2e-utils'; + +/** + * Internal dependencies + */ +import { ProductFiltersPage } from './product-filters.page'; + +const blockData = { + name: 'woocommerce/product-filter-attribute', + selectors: { + frontend: {}, + editor: { + settings: {}, + }, + }, + slug: 'archive-product', +}; + +const test = base.extend< { pageObject: ProductFiltersPage } >( { + pageObject: async ( { page, editor, frontendUtils }, use ) => { + const pageObject = new ProductFiltersPage( { + page, + editor, + frontendUtils, + } ); + await use( pageObject ); + }, +} ); + +test.describe( `${ blockData.name }`, () => { + test.beforeEach( async ( { admin, requestUtils } ) => { + await requestUtils.activatePlugin( + 'woocommerce-blocks-test-enable-experimental-features' + ); + await admin.visitSiteEditor( { + postId: `woocommerce/woocommerce//${ blockData.slug }`, + postType: 'wp_template', + canvas: 'edit', + } ); + } ); + + test( 'should display the correct inspector style controls', async ( { + editor, + pageObject, + } ) => { + await pageObject.addProductFiltersBlock( { cleanContent: true } ); + + const block = editor.canvas + .getByLabel( 'Block: Attribute (Experimental)' ) + .getByLabel( 'Block: Filter Options' ); + + await expect( block ).toBeVisible(); + + await block.click(); + await editor.openDocumentSettingsSidebar(); + await editor.page.getByRole( 'tab', { name: 'Styles' } ).click(); + + await expect( + editor.page.getByText( 'ColorAll options are currently hidden' ) + ).toBeVisible(); + await expect( + editor.page.getByText( + 'TypographyAll options are currently hidden' + ) + ).toBeVisible(); + await expect( + editor.page.getByText( + 'DimensionsAll options are currently hidden' + ) + ).toBeVisible(); + await expect( + editor.page.getByText( 'DisplayListChips' ) + ).toBeVisible(); + } ); + + test( 'should display the correct inspector setting controls', async ( { + editor, + pageObject, + } ) => { + await pageObject.addProductFiltersBlock( { cleanContent: true } ); + + const block = editor.canvas + .getByLabel( 'Block: Attribute (Experimental)' ) + .getByLabel( 'Block: Filter Options' ); + + await expect( block ).toBeVisible(); + + await editor.openDocumentSettingsSidebar(); + await block.click(); + + await expect( + editor.page.getByLabel( 'Editor settings' ).getByRole( 'button', { + name: 'Attribute', + exact: true, + } ) + ).toBeVisible(); + await expect( + editor.page + .getByLabel( 'Editor settings' ) + .getByRole( 'button', { name: 'Settings', exact: true } ) + ).toBeVisible(); + await expect( editor.page.getByText( 'Sort order' ) ).toBeVisible(); + await expect( editor.page.getByText( 'LogicAnyAll' ) ).toBeVisible(); + } ); + + test( 'should dynamically set block title and heading based on the selected attribute', async ( { + editor, + pageObject, + } ) => { + await pageObject.addProductFiltersBlock( { cleanContent: true } ); + + const block = editor.canvas + .getByLabel( 'Block: Attribute (Experimental)' ) + .getByLabel( 'Block: Filter Options' ); + + await editor.openDocumentSettingsSidebar(); + await block.click(); + + await editor.page + .getByRole( 'tabpanel' ) + .getByRole( 'combobox' ) + .first() + .click(); + await editor.page + .getByRole( 'option', { name: 'Size', exact: true } ) + .click(); + + await pageObject.page.getByLabel( 'Document Overview' ).click(); + const listView = pageObject.page.getByLabel( 'List View' ); + + await expect( listView ).toBeVisible(); + + const productFilterAttributeSizeBlockListItem = listView.getByText( + 'Size (Experimental)' // it must select the attribute with the highest product count + ); + await expect( productFilterAttributeSizeBlockListItem ).toBeVisible(); + + const productFilterAttributeWrapperBlock = editor.canvas.getByLabel( + 'Block: Attribute (Experimental)' + ); + await editor.selectBlocks( productFilterAttributeWrapperBlock ); + await expect( productFilterAttributeWrapperBlock ).toBeVisible(); + + const productFilterAttributeBlockHeading = + productFilterAttributeWrapperBlock.getByText( 'Size', { + exact: true, + } ); + + await expect( productFilterAttributeBlockHeading ).toBeVisible(); + } ); +} ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts index 001ffc9d04e..1808a3ff867 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts @@ -439,72 +439,4 @@ test.describe( `${ blockData.name }`, () => { block.locator( blockData.selectors.editor.layoutWrapper ) ).toHaveCSS( 'gap', '0px' ); } ); - - test.describe( 'product-filter-attribute', () => { - test( 'should dynamically set block title and heading based on the selected attribute', async ( { - editor, - pageObject, - } ) => { - await pageObject.addProductFiltersBlock( { cleanContent: true } ); - - await pageObject.page.getByLabel( 'Document Overview' ).click(); - const listView = pageObject.page.getByLabel( 'List View' ); - - const productFiltersBlockListItem = listView.getByRole( 'link', { - name: 'Product Filters (Experimental)', - } ); - await expect( productFiltersBlockListItem ).toBeVisible(); - const listViewExpander = - pageObject.page.getByTestId( 'list-view-expander' ); - const listViewExpanderIcon = listViewExpander.locator( 'svg' ); - - await listViewExpanderIcon.click(); - - const productFilterAttributeColorBlockListItem = listView.getByText( - 'Color (Experimental)' // it must select the attribute with the highest product count - ); - await expect( - productFilterAttributeColorBlockListItem - ).toBeVisible(); - - const productFilterAttributeBlock = editor.canvas - .getByLabel( 'Block: Filter Options' ) - .and( - editor.canvas.locator( - '[data-type="woocommerce/product-filter-attribute"]' - ) - ); - await editor.selectBlocks( productFilterAttributeBlock ); - await editor.clickBlockToolbarButton( 'Edit' ); - await editor.canvas - .locator( 'label' ) - .filter( { hasText: 'Size' } ) - .click(); - await editor.canvas.getByRole( 'button', { name: 'Done' } ).click(); - - await expect( - productFilterAttributeColorBlockListItem - ).toBeHidden(); - - const productFilterAttributeSizeBlockListItem = listView.getByText( - 'Size (Experimental)' // it must select the attribute with the highest product count - ); - await expect( - productFilterAttributeSizeBlockListItem - ).toBeVisible(); - - const productFilterAttributeWrapperBlock = editor.canvas.getByLabel( - 'Block: Attribute (Experimental)' - ); - await editor.selectBlocks( productFilterAttributeWrapperBlock ); - await expect( productFilterAttributeWrapperBlock ).toBeVisible(); - - const productFilterAttributeBlockHeading = - productFilterAttributeWrapperBlock.getByText( 'Size', { - exact: true, - } ); - - await expect( productFilterAttributeBlockHeading ).toBeVisible(); - } ); - } ); } ); diff --git a/plugins/woocommerce/changelog/fix-49578-new-attribute-filter-inspector-settings b/plugins/woocommerce/changelog/fix-49578-new-attribute-filter-inspector-settings new file mode 100644 index 00000000000..d7ddbb723c4 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-49578-new-attribute-filter-inspector-settings @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: [Experimental] Attribute Filter: Update Inspector Control Settings + + diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php index ed4bb640c5d..d9f3cb83198 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php @@ -30,6 +30,33 @@ final class ProductFilterAttribute extends AbstractBlock { add_filter( 'collection_filter_query_param_keys', array( $this, 'get_filter_query_param_keys' ), 10, 2 ); add_filter( 'collection_active_filters_data', array( $this, 'register_active_filters_data' ), 10, 2 ); + add_action( 'deleted_transient', array( $this, 'delete_default_attribute_id_transient' ) ); + } + + /** + * Extra data passed through from server to client for block. + * + * @param array $attributes Any attributes that currently are available from the block. + * Note, this will be empty in the editor context when the block is + * not in the post content on editor load. + */ + protected function enqueue_data( array $attributes = array() ) { + parent::enqueue_data( $attributes ); + + if ( is_admin() ) { + $this->asset_data_registry->add( 'defaultProductFilterAttribute', $this->get_default_attribute() ); + } + } + + /** + * Delete the default attribute id transient when the attribute taxonomies are deleted. + * + * @param string $transient The transient name. + */ + public function delete_default_attribute_id_transient( $transient ) { + if ( 'wc_attribute_taxonomies' === $transient ) { + delete_transient( 'wc_block_product_filter_attribute_default_attribute' ); + } } /** @@ -43,7 +70,7 @@ final class ProductFilterAttribute extends AbstractBlock { public function get_filter_query_param_keys( $filter_param_keys, $url_param_keys ) { $attribute_param_keys = array_filter( $url_param_keys, - function( $param ) { + function ( $param ) { return strpos( $param, 'filter_' ) === 0 || strpos( $param, 'query_type_' ) === 0; } ); @@ -64,7 +91,7 @@ final class ProductFilterAttribute extends AbstractBlock { public function register_active_filters_data( $data, $params ) { $product_attributes_map = array_reduce( wc_get_attribute_taxonomies(), - function( $acc, $attribute_object ) { + function ( $acc, $attribute_object ) { $acc[ $attribute_object->attribute_name ] = $attribute_object->attribute_label; return $acc; }, @@ -73,7 +100,7 @@ final class ProductFilterAttribute extends AbstractBlock { $active_product_attributes = array_reduce( array_keys( $params ), - function( $acc, $attribute ) { + function ( $acc, $attribute ) { if ( strpos( $attribute, 'filter_' ) === 0 ) { $acc[] = str_replace( 'filter_', '', $attribute ); } @@ -84,7 +111,7 @@ final class ProductFilterAttribute extends AbstractBlock { $active_product_attributes = array_filter( $active_product_attributes, - function( $item ) use ( $product_attributes_map ) { + function ( $item ) use ( $product_attributes_map ) { return in_array( $item, array_keys( $product_attributes_map ), true ); } ); @@ -96,7 +123,7 @@ final class ProductFilterAttribute extends AbstractBlock { // Get attribute term by slug. $terms = array_map( - function( $term ) use ( $product_attribute, $action_namespace ) { + function ( $term ) use ( $product_attribute, $action_namespace ) { $term_object = get_term_by( 'slug', $term, "pa_{$product_attribute}" ); return array( 'title' => $term_object->name, @@ -168,7 +195,7 @@ final class ProductFilterAttribute extends AbstractBlock { ); $attribute_options = array_map( - function( $term ) use ( $attribute_counts, $selected_terms ) { + function ( $term ) use ( $attribute_counts, $selected_terms ) { $term = (array) $term; $term['count'] = $attribute_counts[ $term['term_id'] ]; $term['selected'] = in_array( $term['slug'], $selected_terms, true ); @@ -179,7 +206,7 @@ final class ProductFilterAttribute extends AbstractBlock { $filtered_options = array_filter( $attribute_options, - function( $option ) { + function ( $option ) { return $option['count'] > 0; } ); @@ -191,7 +218,7 @@ final class ProductFilterAttribute extends AbstractBlock { $context = array( 'attributeSlug' => str_replace( 'pa_', '', $product_attribute->slug ), 'queryType' => $attributes['queryType'], - 'selectType' => $attributes['selectType'], + 'selectType' => 'multiple', ); return sprintf( @@ -242,7 +269,7 @@ final class ProductFilterAttribute extends AbstractBlock { 'items' => $list_items, 'action' => "{$this->get_full_block_name()}::actions.navigate", 'selected_items' => $selected_items, - 'select_type' => $attributes['selectType'] ?? 'multiple', + 'select_type' => 'multiple', // translators: %s is a product attribute name. 'placeholder' => sprintf( __( 'Select %s', 'woocommerce' ), $product_attribute->name ), ) @@ -264,7 +291,7 @@ final class ProductFilterAttribute extends AbstractBlock { $show_counts = $attributes['showCounts'] ?? false; $list_options = array_map( - function( $option ) use ( $show_counts ) { + function ( $option ) use ( $show_counts ) { return array( 'id' => $option['slug'] . '-' . $option['term_id'], 'checked' => $option['selected'], @@ -320,13 +347,72 @@ final class ProductFilterAttribute extends AbstractBlock { $attribute_counts = array_reduce( $attribute_counts, - function( $acc, $count ) { + function ( $acc, $count ) { $acc[ $count['term'] ] = $count['count']; return $acc; }, - [] + array() ); return $attribute_counts; } + + /** + * Get the attribute if with most term but closest to 30 terms. + * + * @return int + */ + private function get_default_attribute() { + $cached = get_transient( 'wc_block_product_filter_attribute_default_attribute' ); + + if ( $cached ) { + return $cached; + } + + $attributes = wc_get_attribute_taxonomies(); + + $attributes_count = array_map( + function ( $attribute ) { + return intval( + wp_count_terms( + array( + 'taxonomy' => 'pa_' . $attribute->attribute_name, + 'hide_empty' => false, + ) + ) + ); + }, + $attributes + ); + + asort( $attributes_count ); + + $search = 30; + $closest = null; + $attribute_id = null; + + foreach ( $attributes_count as $id => $count ) { + if ( null === $closest || abs( $search - $closest ) > abs( $count - $search ) ) { + $closest = $count; + $attribute_id = $id; + } + + if ( $closest && $count >= $search ) { + break; + } + } + + $default_attribute = array( + 'id' => 0, + 'label' => __( 'Attribute', 'woocommerce' ), + ); + + if ( $attribute_id ) { + $default_attribute = $attributes[ $attribute_id ]; + } + + set_transient( 'wc_block_product_filter_attribute_default_attribute', $default_attribute ); + + return $default_attribute; + } }