[Experimental] Attribute Filter: Update Inspector Control Settings (#49751)
* add: new inspector controls * update: cleanup the edit component * chore: remove unused old inspector component * tweak: add formatting to help text as Jarek said: > Here’s the copy. Please mind the formatting. * add: default attribute id that closest to 30 * update: pass the whole attribute object * update: use default attribute term in Product Filters template * add: style settings * add: sorting and show empty behavior in the editor preview * tweak: use hideEmpty to align with existing settings * chore: changelog * test: add basic e2e tests * chore: restore previous default value * add: attribute help text and border support * chore: lint * fix: add missing context
This commit is contained in:
parent
6000a1e83c
commit
234f5d7e49
|
@ -14,7 +14,45 @@
|
||||||
"inserter": false,
|
"inserter": false,
|
||||||
"color": {
|
"color": {
|
||||||
"text": true,
|
"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" ],
|
"usesContext": [ "query", "queryId" ],
|
||||||
|
@ -42,6 +80,18 @@
|
||||||
"isPreview": {
|
"isPreview": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false
|
"default": false
|
||||||
|
},
|
||||||
|
"sortOrder": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "count-desc"
|
||||||
|
},
|
||||||
|
"hideEmpty": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"clearButton": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default":true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (
|
|
||||||
<InspectorControls key="inspector">
|
|
||||||
<PanelBody title={ __( 'Display Settings', 'woocommerce' ) }>
|
|
||||||
<ToggleControl
|
|
||||||
label={ __( 'Display product count', 'woocommerce' ) }
|
|
||||||
checked={ showCounts }
|
|
||||||
onChange={ () =>
|
|
||||||
setAttributes( {
|
|
||||||
showCounts: ! showCounts,
|
|
||||||
} )
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ToggleGroupControl
|
|
||||||
label={ __(
|
|
||||||
'Allow selecting multiple options?',
|
|
||||||
'woocommerce'
|
|
||||||
) }
|
|
||||||
value={ selectType || 'multiple' }
|
|
||||||
onChange={ ( value: string ) =>
|
|
||||||
setAttributes( {
|
|
||||||
selectType: value,
|
|
||||||
} )
|
|
||||||
}
|
|
||||||
className="wc-block-attribute-filter__multiple-toggle"
|
|
||||||
>
|
|
||||||
<ToggleGroupControlOption
|
|
||||||
value="multiple"
|
|
||||||
label={ _x(
|
|
||||||
'Multiple',
|
|
||||||
'Number of filters',
|
|
||||||
'woocommerce'
|
|
||||||
) }
|
|
||||||
/>
|
|
||||||
<ToggleGroupControlOption
|
|
||||||
value="single"
|
|
||||||
label={ _x(
|
|
||||||
'Single',
|
|
||||||
'Number of filters',
|
|
||||||
'woocommerce'
|
|
||||||
) }
|
|
||||||
/>
|
|
||||||
</ToggleGroupControl>
|
|
||||||
{ selectType === 'multiple' && (
|
|
||||||
<ToggleGroupControl
|
|
||||||
label={ __( 'Filter Conditions', 'woocommerce' ) }
|
|
||||||
help={
|
|
||||||
queryType === 'and'
|
|
||||||
? __(
|
|
||||||
'Choose to return filter results for all of the attributes selected.',
|
|
||||||
'woocommerce'
|
|
||||||
)
|
|
||||||
: __(
|
|
||||||
'Choose to return filter results for any of the attributes selected.',
|
|
||||||
'woocommerce'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
value={ queryType }
|
|
||||||
onChange={ ( value: string ) =>
|
|
||||||
setAttributes( {
|
|
||||||
queryType: value,
|
|
||||||
} )
|
|
||||||
}
|
|
||||||
className="wc-block-attribute-filter__conditions-toggle"
|
|
||||||
>
|
|
||||||
<ToggleGroupControlOption
|
|
||||||
value="and"
|
|
||||||
label={ __( 'All', 'woocommerce' ) }
|
|
||||||
/>
|
|
||||||
<ToggleGroupControlOption
|
|
||||||
value="or"
|
|
||||||
label={ __( 'Any', 'woocommerce' ) }
|
|
||||||
/>
|
|
||||||
</ToggleGroupControl>
|
|
||||||
) }
|
|
||||||
<ToggleGroupControl
|
|
||||||
label={ __( 'Display Style', 'woocommerce' ) }
|
|
||||||
value={ displayStyle }
|
|
||||||
onChange={ ( value: string ) =>
|
|
||||||
setAttributes( {
|
|
||||||
displayStyle: value,
|
|
||||||
} )
|
|
||||||
}
|
|
||||||
className="wc-block-attribute-filter__display-toggle"
|
|
||||||
>
|
|
||||||
<ToggleGroupControlOption
|
|
||||||
value="list"
|
|
||||||
label={ __( 'List', 'woocommerce' ) }
|
|
||||||
/>
|
|
||||||
<ToggleGroupControlOption
|
|
||||||
value="dropdown"
|
|
||||||
label={ __( 'Dropdown', 'woocommerce' ) }
|
|
||||||
/>
|
|
||||||
</ToggleGroupControl>
|
|
||||||
</PanelBody>
|
|
||||||
<PanelBody
|
|
||||||
title={ __( 'Content Settings', 'woocommerce' ) }
|
|
||||||
initialOpen={ false }
|
|
||||||
>
|
|
||||||
<AttributeSelectControls
|
|
||||||
isCompact={ true }
|
|
||||||
attributeId={ attributeId }
|
|
||||||
setAttributeId={ ( id: number ) => {
|
|
||||||
setAttributes( {
|
|
||||||
attributeId: id,
|
|
||||||
} );
|
|
||||||
} }
|
|
||||||
/>
|
|
||||||
</PanelBody>
|
|
||||||
</InspectorControls>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<InspectorControls key="inspector">
|
||||||
|
<PanelBody title={ __( 'Attribute', 'woocommerce' ) }>
|
||||||
|
<ComboboxControl
|
||||||
|
options={ ATTRIBUTES.map( ( item ) => ( {
|
||||||
|
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'
|
||||||
|
) }
|
||||||
|
/>
|
||||||
|
</PanelBody>
|
||||||
|
<PanelBody title={ __( 'Settings', 'woocommerce' ) }>
|
||||||
|
<SelectControl
|
||||||
|
label={ __( 'Sort order', 'woocommerce' ) }
|
||||||
|
value={ sortOrder }
|
||||||
|
options={ [
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
label: __( 'Select an option', 'woocommerce' ),
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
...sortOrderOptions,
|
||||||
|
] }
|
||||||
|
onChange={ ( value ) =>
|
||||||
|
setAttributes( { sortOrder: value } )
|
||||||
|
}
|
||||||
|
help={ __(
|
||||||
|
'Determine the order of filter options.',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
/>
|
||||||
|
<ToggleGroupControl
|
||||||
|
label={ __( 'Logic', 'woocommerce' ) }
|
||||||
|
value={ queryType }
|
||||||
|
onChange={ ( value: BlockAttributes[ 'queryType' ] ) =>
|
||||||
|
setAttributes( { queryType: value } )
|
||||||
|
}
|
||||||
|
style={ { width: '100%' } }
|
||||||
|
help={
|
||||||
|
queryType === 'and'
|
||||||
|
? createInterpolateElement(
|
||||||
|
__(
|
||||||
|
'Show results for <b>all</b> selected attributes. Displayed products must contain <b>all of them</b> to appear in the results.',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
b: <strong />,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: __(
|
||||||
|
'Show results for any of the attributes selected (displayed products don’t have to have them all).',
|
||||||
|
'woocommerce'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ToggleGroupControlOption
|
||||||
|
label={ __( 'Any', 'woocommerce' ) }
|
||||||
|
value="or"
|
||||||
|
/>
|
||||||
|
<ToggleGroupControlOption
|
||||||
|
label={ __( 'All', 'woocommerce' ) }
|
||||||
|
value="and"
|
||||||
|
/>
|
||||||
|
</ToggleGroupControl>
|
||||||
|
</PanelBody>
|
||||||
|
</InspectorControls>
|
||||||
|
<InspectorControls group="styles">
|
||||||
|
<PanelBody title={ __( 'Display', 'woocommerce' ) }>
|
||||||
|
<ToggleGroupControl
|
||||||
|
value={ displayStyle }
|
||||||
|
onChange={ (
|
||||||
|
value: BlockAttributes[ 'displayStyle' ]
|
||||||
|
) => setAttributes( { displayStyle: value } ) }
|
||||||
|
style={ { width: '100%' } }
|
||||||
|
>
|
||||||
|
<ToggleGroupControlOption
|
||||||
|
label={ __( 'List', 'woocommerce' ) }
|
||||||
|
value="list"
|
||||||
|
/>
|
||||||
|
<ToggleGroupControlOption
|
||||||
|
label={ __( 'Chips', 'woocommerce' ) }
|
||||||
|
value="chips"
|
||||||
|
/>
|
||||||
|
</ToggleGroupControl>
|
||||||
|
<ToggleControl
|
||||||
|
label={ __( 'Product counts', 'woocommerce' ) }
|
||||||
|
checked={ showCounts }
|
||||||
|
onChange={ ( value ) =>
|
||||||
|
setAttributes( { showCounts: value } )
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ToggleControl
|
||||||
|
label={ __( 'Empty filter options', 'woocommerce' ) }
|
||||||
|
checked={ ! hideEmpty }
|
||||||
|
onChange={ ( value ) =>
|
||||||
|
setAttributes( { hideEmpty: ! value } )
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ToggleControl
|
||||||
|
label={ __( 'Clear button', 'woocommerce' ) }
|
||||||
|
checked={ clearButton }
|
||||||
|
onChange={ ( value ) =>
|
||||||
|
setAttributes( { clearButton: value } )
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PanelBody>
|
||||||
|
</InspectorControls>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,7 +1,12 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
export const attributeOptionsPreview = [
|
export const attributeOptionsPreview = [
|
||||||
{
|
{
|
||||||
id: 23,
|
id: 23,
|
||||||
name: 'Blue',
|
name: __( 'Blue', 'woocommerce' ),
|
||||||
slug: 'blue',
|
slug: 'blue',
|
||||||
attr_slug: 'blue',
|
attr_slug: 'blue',
|
||||||
description: '',
|
description: '',
|
||||||
|
@ -10,7 +15,7 @@ export const attributeOptionsPreview = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 29,
|
id: 29,
|
||||||
name: 'Gray',
|
name: __( 'Gray', 'woocommerce' ),
|
||||||
slug: 'gray',
|
slug: 'gray',
|
||||||
attr_slug: 'gray',
|
attr_slug: 'gray',
|
||||||
description: '',
|
description: '',
|
||||||
|
@ -19,7 +24,7 @@ export const attributeOptionsPreview = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 24,
|
id: 24,
|
||||||
name: 'Green',
|
name: __( 'Green', 'woocommerce' ),
|
||||||
slug: 'green',
|
slug: 'green',
|
||||||
attr_slug: 'green',
|
attr_slug: 'green',
|
||||||
description: '',
|
description: '',
|
||||||
|
@ -28,7 +33,7 @@ export const attributeOptionsPreview = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 25,
|
id: 25,
|
||||||
name: 'Red',
|
name: __( 'Red', 'woocommerce' ),
|
||||||
slug: 'red',
|
slug: 'red',
|
||||||
attr_slug: 'red',
|
attr_slug: 'red',
|
||||||
description: '',
|
description: '',
|
||||||
|
@ -37,7 +42,7 @@ export const attributeOptionsPreview = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 30,
|
id: 30,
|
||||||
name: 'Yellow',
|
name: __( 'Yellow', 'woocommerce' ),
|
||||||
slug: 'yellow',
|
slug: 'yellow',
|
||||||
attr_slug: 'yellow',
|
attr_slug: 'yellow',
|
||||||
description: '',
|
description: '',
|
||||||
|
@ -45,3 +50,17 @@ export const attributeOptionsPreview = [
|
||||||
count: 1,
|
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,
|
||||||
|
} )
|
||||||
|
);
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __, sprintf } from '@wordpress/i18n';
|
import { __, sprintf } from '@wordpress/i18n';
|
||||||
import { useCallback, useEffect, useState } from '@wordpress/element';
|
import { useEffect, useState } from '@wordpress/element';
|
||||||
import { BlockControls, useBlockProps } from '@wordpress/block-editor';
|
import { useBlockProps } from '@wordpress/block-editor';
|
||||||
import { getSetting } from '@woocommerce/settings';
|
import { getSetting } from '@woocommerce/settings';
|
||||||
import {
|
import {
|
||||||
useCollection,
|
useCollection,
|
||||||
|
@ -14,26 +14,16 @@ import {
|
||||||
AttributeTerm,
|
AttributeTerm,
|
||||||
objectHasProp,
|
objectHasProp,
|
||||||
} from '@woocommerce/types';
|
} from '@woocommerce/types';
|
||||||
import {
|
import { Disabled, withSpokenMessages, Notice } from '@wordpress/components';
|
||||||
Disabled,
|
|
||||||
Button,
|
|
||||||
ToolbarGroup,
|
|
||||||
withSpokenMessages,
|
|
||||||
Notice,
|
|
||||||
} from '@wordpress/components';
|
|
||||||
import { dispatch, useSelect } from '@wordpress/data';
|
import { dispatch, useSelect } from '@wordpress/data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { EditProps, isAttributeCounts } from './types';
|
import { EditProps, isAttributeCounts } from './types';
|
||||||
import {
|
import { NoAttributesPlaceholder } from './components/placeholder';
|
||||||
NoAttributesPlaceholder,
|
|
||||||
AttributesPlaceholder,
|
|
||||||
} from './components/placeholder';
|
|
||||||
import { AttributeSelectControls } from './components/attribute-select-controls';
|
|
||||||
import { getAttributeFromId } from './utils';
|
import { getAttributeFromId } from './utils';
|
||||||
import { Inspector } from './components/inspector-controls';
|
import { Inspector } from './components/inspector';
|
||||||
import { AttributeCheckboxList } from './components/attribute-checkbox-list';
|
import { AttributeCheckboxList } from './components/attribute-checkbox-list';
|
||||||
import { AttributeDropdown } from './components/attribute-dropdown';
|
import { AttributeDropdown } from './components/attribute-dropdown';
|
||||||
import { attributeOptionsPreview } from './constants';
|
import { attributeOptionsPreview } from './constants';
|
||||||
|
@ -41,84 +31,21 @@ import './style.scss';
|
||||||
|
|
||||||
const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
|
const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
|
||||||
|
|
||||||
const Toolbar = ( {
|
|
||||||
onClick,
|
|
||||||
isEditing,
|
|
||||||
}: {
|
|
||||||
onClick: () => void;
|
|
||||||
isEditing: boolean;
|
|
||||||
} ) => (
|
|
||||||
<BlockControls>
|
|
||||||
<ToolbarGroup
|
|
||||||
controls={ [
|
|
||||||
{
|
|
||||||
icon: 'edit',
|
|
||||||
title: __( 'Edit', 'woocommerce' ),
|
|
||||||
onClick,
|
|
||||||
isActive: isEditing,
|
|
||||||
},
|
|
||||||
] }
|
|
||||||
/>
|
|
||||||
</BlockControls>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Wrapper = ( {
|
|
||||||
children,
|
|
||||||
onClickToolbarEdit,
|
|
||||||
isEditing,
|
|
||||||
blockProps,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
onClickToolbarEdit: () => void;
|
|
||||||
isEditing: boolean;
|
|
||||||
blockProps: object;
|
|
||||||
} ) => (
|
|
||||||
<div { ...blockProps }>
|
|
||||||
<Toolbar onClick={ onClickToolbarEdit } isEditing={ isEditing } />
|
|
||||||
{ children }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const AttributeSelectPlaceholder = ( {
|
|
||||||
attributeId,
|
|
||||||
setAttributeId,
|
|
||||||
onClickDone,
|
|
||||||
}: {
|
|
||||||
attributeId: number;
|
|
||||||
setAttributeId: ( id: number ) => void;
|
|
||||||
onClickDone: () => void;
|
|
||||||
} ) => (
|
|
||||||
<AttributesPlaceholder>
|
|
||||||
<div className="wc-block-attribute-filter__selection">
|
|
||||||
<AttributeSelectControls
|
|
||||||
isCompact={ false }
|
|
||||||
attributeId={ attributeId }
|
|
||||||
setAttributeId={ setAttributeId }
|
|
||||||
/>
|
|
||||||
<Button variant="primary" onClick={ onClickDone }>
|
|
||||||
{ __( 'Done', 'woocommerce' ) }
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</AttributesPlaceholder>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Edit = ( props: EditProps ) => {
|
const Edit = ( props: EditProps ) => {
|
||||||
const {
|
const { attributes: blockAttributes, clientId } = props;
|
||||||
attributes: blockAttributes,
|
|
||||||
setAttributes,
|
|
||||||
debouncedSpeak,
|
|
||||||
clientId,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const { attributeId, queryType, isPreview, displayStyle, showCounts } =
|
const {
|
||||||
blockAttributes;
|
attributeId,
|
||||||
|
queryType,
|
||||||
|
isPreview,
|
||||||
|
displayStyle,
|
||||||
|
showCounts,
|
||||||
|
sortOrder,
|
||||||
|
hideEmpty,
|
||||||
|
} = blockAttributes;
|
||||||
|
|
||||||
const attributeObject = getAttributeFromId( attributeId );
|
const attributeObject = getAttributeFromId( attributeId );
|
||||||
|
|
||||||
const [ isEditing, setIsEditing ] = useState(
|
|
||||||
! attributeId && ! isPreview
|
|
||||||
);
|
|
||||||
|
|
||||||
const [ attributeOptions, setAttributeOptions ] = useState<
|
const [ attributeOptions, setAttributeOptions ] = useState<
|
||||||
AttributeTerm[]
|
AttributeTerm[]
|
||||||
>( [] );
|
>( [] );
|
||||||
|
@ -130,7 +57,7 @@ const Edit = ( props: EditProps ) => {
|
||||||
resourceName: 'products/attributes/terms',
|
resourceName: 'products/attributes/terms',
|
||||||
resourceValues: [ attributeObject?.id || 0 ],
|
resourceValues: [ attributeObject?.id || 0 ],
|
||||||
shouldSelect: blockAttributes.attributeId > 0,
|
shouldSelect: blockAttributes.attributeId > 0,
|
||||||
query: { orderby: 'menu_order' },
|
query: { orderby: 'menu_order', hide_empty: hideEmpty },
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const { results: filteredCounts } = useCollectionData( {
|
const { results: filteredCounts } = useCollectionData( {
|
||||||
|
@ -183,8 +110,6 @@ const Edit = ( props: EditProps ) => {
|
||||||
[ clientId ]
|
[ clientId ]
|
||||||
);
|
);
|
||||||
|
|
||||||
const blockProps = useBlockProps();
|
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
const termIdHasProducts =
|
const termIdHasProducts =
|
||||||
objectHasProp( filteredCounts, 'attribute_counts' ) &&
|
objectHasProp( filteredCounts, 'attribute_counts' ) &&
|
||||||
|
@ -192,14 +117,31 @@ const Edit = ( props: EditProps ) => {
|
||||||
? filteredCounts.attribute_counts.map( ( term ) => term.term )
|
? filteredCounts.attribute_counts.map( ( term ) => term.term )
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
if ( termIdHasProducts.length === 0 ) return setAttributeOptions( [] );
|
if ( termIdHasProducts.length === 0 && hideEmpty )
|
||||||
|
return setAttributeOptions( [] );
|
||||||
|
|
||||||
setAttributeOptions(
|
setAttributeOptions(
|
||||||
attributeTerms.filter( ( term ) => {
|
attributeTerms
|
||||||
return termIdHasProducts.includes( term.id );
|
.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( () => {
|
useEffect( () => {
|
||||||
if ( productFilterWrapperBlockId ) {
|
if ( productFilterWrapperBlockId ) {
|
||||||
|
@ -231,36 +173,16 @@ const Edit = ( props: EditProps ) => {
|
||||||
updateBlockAttributes,
|
updateBlockAttributes,
|
||||||
] );
|
] );
|
||||||
|
|
||||||
const onClickDone = useCallback( () => {
|
const Wrapper = ( { children }: { children: React.ReactNode } ) => (
|
||||||
setIsEditing( false );
|
<div { ...useBlockProps() }>
|
||||||
debouncedSpeak(
|
<Inspector { ...props } />
|
||||||
__(
|
{ children }
|
||||||
'Now displaying a preview of the Filter Products by Attribute block.',
|
</div>
|
||||||
'woocommerce'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}, [ setIsEditing ] );
|
|
||||||
|
|
||||||
const setAttributeId = useCallback(
|
|
||||||
( id ) => {
|
|
||||||
setAttributes( {
|
|
||||||
attributeId: id,
|
|
||||||
} );
|
|
||||||
},
|
|
||||||
[ setAttributes ]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleEditing = useCallback( () => {
|
|
||||||
setIsEditing( ! isEditing );
|
|
||||||
}, [ isEditing ] );
|
|
||||||
|
|
||||||
if ( isPreview ) {
|
if ( isPreview ) {
|
||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper>
|
||||||
onClickToolbarEdit={ toggleEditing }
|
|
||||||
isEditing={ isEditing }
|
|
||||||
blockProps={ blockProps }
|
|
||||||
>
|
|
||||||
<Disabled>
|
<Disabled>
|
||||||
<AttributeCheckboxList
|
<AttributeCheckboxList
|
||||||
showCounts={ showCounts }
|
showCounts={ showCounts }
|
||||||
|
@ -274,37 +196,14 @@ const Edit = ( props: EditProps ) => {
|
||||||
// Block rendering starts.
|
// Block rendering starts.
|
||||||
if ( Object.keys( ATTRIBUTES ).length === 0 )
|
if ( Object.keys( ATTRIBUTES ).length === 0 )
|
||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper>
|
||||||
onClickToolbarEdit={ toggleEditing }
|
|
||||||
isEditing={ isEditing }
|
|
||||||
blockProps={ blockProps }
|
|
||||||
>
|
|
||||||
<NoAttributesPlaceholder />
|
<NoAttributesPlaceholder />
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
if ( isEditing )
|
|
||||||
return (
|
|
||||||
<Wrapper
|
|
||||||
onClickToolbarEdit={ toggleEditing }
|
|
||||||
isEditing={ isEditing }
|
|
||||||
blockProps={ blockProps }
|
|
||||||
>
|
|
||||||
<AttributeSelectPlaceholder
|
|
||||||
onClickDone={ onClickDone }
|
|
||||||
attributeId={ attributeId }
|
|
||||||
setAttributeId={ setAttributeId }
|
|
||||||
/>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( ! attributeId || ! attributeObject )
|
if ( ! attributeId || ! attributeObject )
|
||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper>
|
||||||
onClickToolbarEdit={ toggleEditing }
|
|
||||||
isEditing={ isEditing }
|
|
||||||
blockProps={ blockProps }
|
|
||||||
>
|
|
||||||
<Notice status="warning" isDismissible={ false }>
|
<Notice status="warning" isDismissible={ false }>
|
||||||
<p>
|
<p>
|
||||||
{ __(
|
{ __(
|
||||||
|
@ -318,11 +217,7 @@ const Edit = ( props: EditProps ) => {
|
||||||
|
|
||||||
if ( attributeOptions.length === 0 )
|
if ( attributeOptions.length === 0 )
|
||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper>
|
||||||
onClickToolbarEdit={ toggleEditing }
|
|
||||||
isEditing={ isEditing }
|
|
||||||
blockProps={ blockProps }
|
|
||||||
>
|
|
||||||
<Notice status="warning" isDismissible={ false }>
|
<Notice status="warning" isDismissible={ false }>
|
||||||
<p>
|
<p>
|
||||||
{ __(
|
{ __(
|
||||||
|
@ -335,12 +230,7 @@ const Edit = ( props: EditProps ) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper>
|
||||||
onClickToolbarEdit={ toggleEditing }
|
|
||||||
isEditing={ isEditing }
|
|
||||||
blockProps={ blockProps }
|
|
||||||
>
|
|
||||||
<Inspector { ...props } />
|
|
||||||
<Disabled>
|
<Disabled>
|
||||||
{ displayStyle === 'dropdown' ? (
|
{ displayStyle === 'dropdown' ? (
|
||||||
<AttributeDropdown
|
<AttributeDropdown
|
||||||
|
|
|
@ -1,20 +1,33 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { registerBlockType } from '@wordpress/blocks';
|
|
||||||
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
|
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
|
||||||
import { productFilterOptions } from '@woocommerce/icons';
|
import { productFilterOptions } from '@woocommerce/icons';
|
||||||
|
import { getSetting } from '@woocommerce/settings';
|
||||||
|
import { registerBlockType } from '@wordpress/blocks';
|
||||||
|
import { AttributeSetting } from '@woocommerce/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import './style.scss';
|
|
||||||
import metadata from './block.json';
|
import metadata from './block.json';
|
||||||
import Edit from './edit';
|
import Edit from './edit';
|
||||||
|
import './style.scss';
|
||||||
|
|
||||||
if ( isExperimentalBlocksEnabled() ) {
|
if ( isExperimentalBlocksEnabled() ) {
|
||||||
|
const defaultAttribute = getSetting< AttributeSetting >(
|
||||||
|
'defaultProductFilterAttribute'
|
||||||
|
);
|
||||||
|
|
||||||
registerBlockType( metadata, {
|
registerBlockType( metadata, {
|
||||||
edit: Edit,
|
edit: Edit,
|
||||||
icon: productFilterOptions,
|
icon: productFilterOptions,
|
||||||
|
attributes: {
|
||||||
|
...metadata.attributes,
|
||||||
|
attributeId: {
|
||||||
|
...metadata.attributes.attributeId,
|
||||||
|
default: parseInt( defaultAttribute.attribute_id, 10 ),
|
||||||
|
},
|
||||||
|
},
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,21 @@
|
||||||
*/
|
*/
|
||||||
import { BlockEditProps } from '@wordpress/blocks';
|
import { BlockEditProps } from '@wordpress/blocks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { sortOrders } from './constants';
|
||||||
|
|
||||||
export type BlockAttributes = {
|
export type BlockAttributes = {
|
||||||
attributeId: number;
|
attributeId: number;
|
||||||
showCounts: boolean;
|
showCounts: boolean;
|
||||||
queryType: string;
|
queryType: 'or' | 'and';
|
||||||
displayStyle: string;
|
displayStyle: string;
|
||||||
selectType: string;
|
selectType: string;
|
||||||
isPreview: boolean;
|
isPreview: boolean;
|
||||||
|
sortOrder: keyof typeof sortOrders;
|
||||||
|
hideEmpty: boolean;
|
||||||
|
clearButton: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface EditProps extends BlockEditProps< BlockAttributes > {
|
export interface EditProps extends BlockEditProps< BlockAttributes > {
|
||||||
|
|
|
@ -1,34 +1,40 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* 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 { filter, filterThreeLines } from '@woocommerce/icons';
|
||||||
import { getSetting } from '@woocommerce/settings';
|
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
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import type { BlockAttributes } from './types';
|
|
||||||
import './editor.scss';
|
import './editor.scss';
|
||||||
|
import type { BlockAttributes } from './types';
|
||||||
|
|
||||||
|
const defaultAttribute = getSetting< AttributeSetting >(
|
||||||
|
'defaultProductFilterAttribute'
|
||||||
|
);
|
||||||
|
|
||||||
const TEMPLATE: InnerBlockTemplate[] = [
|
const TEMPLATE: InnerBlockTemplate[] = [
|
||||||
[
|
[
|
||||||
|
@ -64,8 +70,8 @@ const TEMPLATE: InnerBlockTemplate[] = [
|
||||||
'woocommerce/product-filter',
|
'woocommerce/product-filter',
|
||||||
{
|
{
|
||||||
filterType: 'attribute-filter',
|
filterType: 'attribute-filter',
|
||||||
heading: __( 'Attribute', 'woocommerce' ),
|
heading: defaultAttribute.attribute_label,
|
||||||
attributeId: 0,
|
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 = ( {
|
export const Edit = ( {
|
||||||
setAttributes,
|
setAttributes,
|
||||||
attributes,
|
attributes,
|
||||||
}: BlockEditProps< BlockAttributes > ) => {
|
}: BlockEditProps< BlockAttributes > ) => {
|
||||||
const blockProps = useBlockProps();
|
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 <Spinner />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const templatePartEditUri = getSetting< string >(
|
const templatePartEditUri = getSetting< string >(
|
||||||
'templatePartProductFiltersOverlayEditUri',
|
'templatePartProductFiltersOverlayEditUri',
|
||||||
|
@ -329,7 +273,7 @@ export const Edit = ( {
|
||||||
) }
|
) }
|
||||||
</PanelBody>
|
</PanelBody>
|
||||||
</InspectorControls>
|
</InspectorControls>
|
||||||
<InnerBlocks templateLock={ false } template={ updatedTemplate } />
|
<InnerBlocks templateLock={ false } template={ TEMPLATE } />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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();
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -439,72 +439,4 @@ test.describe( `${ blockData.name }`, () => {
|
||||||
block.locator( blockData.selectors.editor.layoutWrapper )
|
block.locator( blockData.selectors.editor.layoutWrapper )
|
||||||
).toHaveCSS( 'gap', '0px' );
|
).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();
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: update
|
||||||
|
Comment: [Experimental] Attribute Filter: Update Inspector Control Settings
|
||||||
|
|
||||||
|
|
|
@ -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_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_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 ) {
|
public function get_filter_query_param_keys( $filter_param_keys, $url_param_keys ) {
|
||||||
$attribute_param_keys = array_filter(
|
$attribute_param_keys = array_filter(
|
||||||
$url_param_keys,
|
$url_param_keys,
|
||||||
function( $param ) {
|
function ( $param ) {
|
||||||
return strpos( $param, 'filter_' ) === 0 || strpos( $param, 'query_type_' ) === 0;
|
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 ) {
|
public function register_active_filters_data( $data, $params ) {
|
||||||
$product_attributes_map = array_reduce(
|
$product_attributes_map = array_reduce(
|
||||||
wc_get_attribute_taxonomies(),
|
wc_get_attribute_taxonomies(),
|
||||||
function( $acc, $attribute_object ) {
|
function ( $acc, $attribute_object ) {
|
||||||
$acc[ $attribute_object->attribute_name ] = $attribute_object->attribute_label;
|
$acc[ $attribute_object->attribute_name ] = $attribute_object->attribute_label;
|
||||||
return $acc;
|
return $acc;
|
||||||
},
|
},
|
||||||
|
@ -73,7 +100,7 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||||
|
|
||||||
$active_product_attributes = array_reduce(
|
$active_product_attributes = array_reduce(
|
||||||
array_keys( $params ),
|
array_keys( $params ),
|
||||||
function( $acc, $attribute ) {
|
function ( $acc, $attribute ) {
|
||||||
if ( strpos( $attribute, 'filter_' ) === 0 ) {
|
if ( strpos( $attribute, 'filter_' ) === 0 ) {
|
||||||
$acc[] = str_replace( 'filter_', '', $attribute );
|
$acc[] = str_replace( 'filter_', '', $attribute );
|
||||||
}
|
}
|
||||||
|
@ -84,7 +111,7 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||||
|
|
||||||
$active_product_attributes = array_filter(
|
$active_product_attributes = array_filter(
|
||||||
$active_product_attributes,
|
$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 );
|
return in_array( $item, array_keys( $product_attributes_map ), true );
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -96,7 +123,7 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||||
|
|
||||||
// Get attribute term by slug.
|
// Get attribute term by slug.
|
||||||
$terms = array_map(
|
$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}" );
|
$term_object = get_term_by( 'slug', $term, "pa_{$product_attribute}" );
|
||||||
return array(
|
return array(
|
||||||
'title' => $term_object->name,
|
'title' => $term_object->name,
|
||||||
|
@ -168,7 +195,7 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||||
);
|
);
|
||||||
|
|
||||||
$attribute_options = array_map(
|
$attribute_options = array_map(
|
||||||
function( $term ) use ( $attribute_counts, $selected_terms ) {
|
function ( $term ) use ( $attribute_counts, $selected_terms ) {
|
||||||
$term = (array) $term;
|
$term = (array) $term;
|
||||||
$term['count'] = $attribute_counts[ $term['term_id'] ];
|
$term['count'] = $attribute_counts[ $term['term_id'] ];
|
||||||
$term['selected'] = in_array( $term['slug'], $selected_terms, true );
|
$term['selected'] = in_array( $term['slug'], $selected_terms, true );
|
||||||
|
@ -179,7 +206,7 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||||
|
|
||||||
$filtered_options = array_filter(
|
$filtered_options = array_filter(
|
||||||
$attribute_options,
|
$attribute_options,
|
||||||
function( $option ) {
|
function ( $option ) {
|
||||||
return $option['count'] > 0;
|
return $option['count'] > 0;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -191,7 +218,7 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||||
$context = array(
|
$context = array(
|
||||||
'attributeSlug' => str_replace( 'pa_', '', $product_attribute->slug ),
|
'attributeSlug' => str_replace( 'pa_', '', $product_attribute->slug ),
|
||||||
'queryType' => $attributes['queryType'],
|
'queryType' => $attributes['queryType'],
|
||||||
'selectType' => $attributes['selectType'],
|
'selectType' => 'multiple',
|
||||||
);
|
);
|
||||||
|
|
||||||
return sprintf(
|
return sprintf(
|
||||||
|
@ -242,7 +269,7 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||||
'items' => $list_items,
|
'items' => $list_items,
|
||||||
'action' => "{$this->get_full_block_name()}::actions.navigate",
|
'action' => "{$this->get_full_block_name()}::actions.navigate",
|
||||||
'selected_items' => $selected_items,
|
'selected_items' => $selected_items,
|
||||||
'select_type' => $attributes['selectType'] ?? 'multiple',
|
'select_type' => 'multiple',
|
||||||
// translators: %s is a product attribute name.
|
// translators: %s is a product attribute name.
|
||||||
'placeholder' => sprintf( __( 'Select %s', 'woocommerce' ), $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;
|
$show_counts = $attributes['showCounts'] ?? false;
|
||||||
|
|
||||||
$list_options = array_map(
|
$list_options = array_map(
|
||||||
function( $option ) use ( $show_counts ) {
|
function ( $option ) use ( $show_counts ) {
|
||||||
return array(
|
return array(
|
||||||
'id' => $option['slug'] . '-' . $option['term_id'],
|
'id' => $option['slug'] . '-' . $option['term_id'],
|
||||||
'checked' => $option['selected'],
|
'checked' => $option['selected'],
|
||||||
|
@ -320,13 +347,72 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||||
|
|
||||||
$attribute_counts = array_reduce(
|
$attribute_counts = array_reduce(
|
||||||
$attribute_counts,
|
$attribute_counts,
|
||||||
function( $acc, $count ) {
|
function ( $acc, $count ) {
|
||||||
$acc[ $count['term'] ] = $count['count'];
|
$acc[ $count['term'] ] = $count['count'];
|
||||||
return $acc;
|
return $acc;
|
||||||
},
|
},
|
||||||
[]
|
array()
|
||||||
);
|
);
|
||||||
|
|
||||||
return $attribute_counts;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue