AttributePicker: Set default values if they exist (https://github.com/woocommerce/woocommerce-blocks/pull/4815)

* Update API to add default boolean to attribute values and use this within the component to set the default attributes.

* Modify and add unit tests for isObject and getDefaultAttributes

* Sanitize attribute name to accommodate for custom attributes being default values.

* Comments for sanitized_attribute_name variable

* Remove second argument from getAttributes
This commit is contained in:
Tom Cafferkey 2021-10-06 09:48:13 +01:00 committed by GitHub
parent 97c8b7dee5
commit ee84901ab8
7 changed files with 178 additions and 8 deletions

View File

@ -11,6 +11,7 @@ import AttributeSelectControl from './attribute-select-control';
import {
getVariationMatchingSelectedAttributes,
getActiveSelectControlOptions,
getDefaultAttributes,
} from './utils';
/**
@ -27,6 +28,7 @@ const AttributePicker = ( {
const currentVariationAttributes = useShallowEqual( variationAttributes );
const [ variationId, setVariationId ] = useState( 0 );
const [ selectedAttributes, setSelectedAttributes ] = useState( {} );
const [ hasSetDefaults, setHasSetDefaults ] = useState( false );
// Get options for each attribute picker.
const filteredAttributeOptions = useMemo( () => {
@ -37,6 +39,19 @@ const AttributePicker = ( {
);
}, [ selectedAttributes, currentAttributes, currentVariationAttributes ] );
// Set default attributes as selected.
useEffect( () => {
if ( ! hasSetDefaults ) {
const defaultAttributes = getDefaultAttributes( attributes );
if ( defaultAttributes ) {
setSelectedAttributes( {
...defaultAttributes,
} );
}
setHasSetDefaults( true );
}
}, [ selectedAttributes, attributes, hasSetDefaults ] );
// Select variations when selections are change.
useEffect( () => {
const hasSelectedAllAttributes =

View File

@ -15,7 +15,6 @@ import { getAttributes, getVariationAttributes } from './utils';
const VariationAttributes = ( { product, dispatchers } ) => {
const attributes = getAttributes( product.attributes );
const variationAttributes = getVariationAttributes( product.variations );
if (
Object.keys( attributes ).length === 0 ||
variationAttributes.length === 0

View File

@ -7,6 +7,7 @@ import {
getVariationsMatchingSelectedAttributes,
getVariationMatchingSelectedAttributes,
getActiveSelectControlOptions,
getDefaultAttributes,
} from '../utils';
const rawAttributeData = [
@ -20,16 +21,19 @@ const rawAttributeData = [
id: 22,
name: 'Blue',
slug: 'blue',
default: true,
},
{
id: 23,
name: 'Green',
slug: 'green',
default: false,
},
{
id: 24,
name: 'Red',
slug: 'red',
default: false,
},
],
},
@ -43,11 +47,13 @@ const rawAttributeData = [
id: 0,
name: 'Yes',
slug: 'Yes',
default: true,
},
{
id: 0,
name: 'No',
slug: 'No',
default: false,
},
],
},
@ -61,11 +67,13 @@ const rawAttributeData = [
id: 0,
name: 'Test',
slug: 'Test',
default: false,
},
{
id: 0,
name: 'Test 2',
slug: 'Test 2',
default: false,
},
],
},
@ -126,6 +134,61 @@ const rawVariations = [
},
];
const formattedAttributes = {
Color: {
id: 1,
name: 'Color',
taxonomy: 'pa_color',
has_variations: true,
terms: [
{
id: 22,
name: 'Blue',
slug: 'blue',
default: true,
},
{
id: 23,
name: 'Green',
slug: 'green',
default: false,
},
{
id: 24,
name: 'Red',
slug: 'red',
default: false,
},
],
},
Size: {
id: 2,
name: 'Size',
taxonomy: 'pa_size',
has_variations: true,
terms: [
{
id: 25,
name: 'Large',
slug: 'large',
default: false,
},
{
id: 26,
name: 'Medium',
slug: 'medium',
default: true,
},
{
id: 27,
name: 'Small',
slug: 'small',
default: false,
},
],
},
};
describe( 'Testing utils', () => {
describe( 'Testing getAttributes()', () => {
it( 'returns empty object if there are no attributes', () => {
@ -145,16 +208,19 @@ describe( 'Testing utils', () => {
id: 22,
name: 'Blue',
slug: 'blue',
default: true,
},
{
id: 23,
name: 'Green',
slug: 'green',
default: false,
},
{
id: 24,
name: 'Red',
slug: 'red',
default: false,
},
],
},
@ -168,11 +234,13 @@ describe( 'Testing utils', () => {
id: 0,
name: 'Yes',
slug: 'Yes',
default: true,
},
{
id: 0,
name: 'No',
slug: 'No',
default: false,
},
],
},
@ -392,4 +460,20 @@ describe( 'Testing utils', () => {
} );
} );
} );
describe( 'Testing getDefaultAttributes()', () => {
const defaultAttributes = getDefaultAttributes( formattedAttributes );
it( 'should return default attributes in the format that is ready for setting state', () => {
expect( defaultAttributes ).toStrictEqual( {
Color: 'blue',
Size: 'medium',
} );
} );
it( 'should return an empty object if given unexpected values', () => {
expect( getDefaultAttributes( [] ) ).toStrictEqual( {} );
expect( getDefaultAttributes( null ) ).toStrictEqual( {} );
expect( getDefaultAttributes( undefined ) ).toStrictEqual( {} );
} );
} );
} );

View File

@ -3,6 +3,7 @@
*/
import { keyBy } from 'lodash';
import { decodeEntities } from '@wordpress/html-entities';
import { isObject } from '@woocommerce/types';
/**
* Key an array of attributes by name,
@ -206,3 +207,34 @@ export const getActiveSelectControlOptions = (
return options;
};
/**
* Return the default values of the given attributes in a format ready to be set in state.
*
* @param {Object} attributes List of attribute names and terms.
* @return {Object} Default attributes.
*/
export const getDefaultAttributes = ( attributes = {} ) => {
if ( ! isObject( attributes ) ) {
return {};
}
const attributeNames = Object.keys( attributes );
const defaultsToSet = {};
if ( attributeNames.length === 0 ) {
return defaultsToSet;
}
attributeNames.forEach( ( attributeName ) => {
const currentAttribute = attributes[ attributeName ];
const defaultValue = currentAttribute.terms.filter(
( term ) => term.default
);
if ( defaultValue.length > 0 ) {
defaultsToSet[ currentAttribute.name ] = defaultValue[ 0 ]?.slug;
}
} );
return defaultsToSet;
};

View File

@ -13,7 +13,11 @@ export const isString = < U >( term: string | U ): term is string => {
export const isObject = < T extends Record< string, unknown >, U >(
term: T | U
): term is NonNullable< T > => {
return ! isNull( term ) && typeof term === 'object';
return (
! isNull( term ) &&
term instanceof Object &&
term.constructor === Object
);
};
export function objectHasProp< P extends PropertyKey >(

View File

@ -0,0 +1,17 @@
/**
* External dependencies
*/
import { isObject } from '@woocommerce/types';
describe( 'type-guards', () => {
describe( 'Testing isObject()', () => {
it( 'Correctly identifies an object', () => {
expect( isObject( {} ) ).toBe( true );
expect( isObject( { test: 'object' } ) ).toBe( true );
} );
it( 'Correctly rejects object-like things', () => {
expect( isObject( [] ) ).toBe( false );
expect( isObject( null ) ).toBe( false );
} );
} );
} );

View File

@ -288,24 +288,30 @@ class ProductSchema extends AbstractSchema {
'items' => [
'type' => 'object',
'properties' => [
'id' => [
'id' => [
'description' => __( 'The term ID, or 0 if the attribute is not a global attribute.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'name' => [
'name' => [
'description' => __( 'The term name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'slug' => [
'slug' => [
'description' => __( 'The term slug.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'default' => [
'description' => __( 'If this is a default attribute', 'woo-gutenberg-products-block' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
@ -623,20 +629,33 @@ class ProductSchema extends AbstractSchema {
* @return array
*/
protected function get_attributes( \WC_Product $product ) {
$attributes = array_filter( $product->get_attributes(), [ $this, 'filter_valid_attribute' ] );
$return = [];
$attributes = array_filter( $product->get_attributes(), [ $this, 'filter_valid_attribute' ] );
$default_attributes = $product->get_default_attributes();
$return = [];
foreach ( $attributes as $attribute_slug => $attribute ) {
// Only visible and variation attributes will be exposed by this API.
if ( ! $attribute->get_visible() || ! $attribute->get_variation() ) {
continue;
}
$terms = $attribute->is_taxonomy() ? array_map( [ $this, 'prepare_product_attribute_taxonomy_value' ], $attribute->get_terms() ) : array_map( [ $this, 'prepare_product_attribute_value' ], $attribute->get_options() );
// Custom attribute names are sanitized to be the array keys.
// So when we do the array_key_exists check below we also need to sanitize the attribute names.
$sanitized_attribute_name = sanitize_key( $attribute->get_name() );
if ( array_key_exists( $sanitized_attribute_name, $default_attributes ) ) {
foreach ( $terms as $term ) {
$term->default = $term->slug === $default_attributes[ $sanitized_attribute_name ];
}
}
$return[] = (object) [
'id' => $attribute->get_id(),
'name' => wc_attribute_label( $attribute->get_name(), $product ),
'taxonomy' => $attribute->is_taxonomy() ? $attribute->get_name() : null,
'has_variations' => true === $attribute->get_variation(),
'terms' => $attribute->is_taxonomy() ? array_map( [ $this, 'prepare_product_attribute_taxonomy_value' ], $attribute->get_terms() ) : array_map( [ $this, 'prepare_product_attribute_value' ], $attribute->get_options() ),
'terms' => $terms,
];
}