Introduce Variation Picker Component (https://github.com/woocommerce/woocommerce-blocks/pull/2497)
* Add attributes to API * Add component * API updates to return variation data * Update to handle updated api responses * Working picker * Update tests * update test * Use SelectControl * Add Picker to Form Block * Code cleanup and splitting * Inline todos * Update todos * Update assets/js/atomic/blocks/product/add-to-cart/product-types/variable/variation-attributes/index.js Co-authored-by: Albert Juhé Lluveras <contact@albertjuhe.com> * change classname and move styles within component * Correct conditional * Avoid nesting filters * Remove exclude from getVariationsMatchingSelectedAttributes * basic select styles * remove custom select styles Co-authored-by: Albert Juhé Lluveras <contact@albertjuhe.com>
This commit is contained in:
parent
1026ff8fd4
commit
98d240d5e6
|
@ -12,7 +12,7 @@ import {
|
||||||
QuantityInput,
|
QuantityInput,
|
||||||
ProductUnavailable,
|
ProductUnavailable,
|
||||||
} from '../../shared';
|
} from '../../shared';
|
||||||
import VariationPicker from './variation-picker';
|
import VariationAttributes from './variation-attributes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Variable Product Add To Cart Form
|
* Variable Product Add To Cart Form
|
||||||
|
@ -44,7 +44,7 @@ const Variable = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<VariationPicker />
|
<VariationAttributes product={ product } />
|
||||||
<QuantityInput
|
<QuantityInput
|
||||||
value={ quantity }
|
value={ quantity }
|
||||||
min={ minQuantity }
|
min={ minQuantity }
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { useState, useEffect, useMemo } from '@wordpress/element';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import AttributeSelectControl from './attribute-select-control';
|
||||||
|
import {
|
||||||
|
getVariationsMatchingSelectedAttributes,
|
||||||
|
getSelectControlOptions,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AttributePicker component.
|
||||||
|
*
|
||||||
|
* @param {*} props Component props.
|
||||||
|
*/
|
||||||
|
const AttributePicker = ( { attributes, variationAttributes } ) => {
|
||||||
|
const [ variationId, setVariationId ] = useState( 0 );
|
||||||
|
|
||||||
|
// @todo Support default selected attributes in Variation Picker.
|
||||||
|
const [ selectedAttributes, setSelectedAttributes ] = useState( [] );
|
||||||
|
|
||||||
|
const attributeNames = Object.keys( attributes );
|
||||||
|
const hasSelectedAttributes =
|
||||||
|
Object.values( selectedAttributes ).filter( Boolean ).length > 0;
|
||||||
|
const hasSelectedAllAttributes =
|
||||||
|
Object.values( selectedAttributes ).filter(
|
||||||
|
( selected ) => selected !== ''
|
||||||
|
).length === attributeNames.length;
|
||||||
|
|
||||||
|
// Gets valid attribute options for the picker taking current selections into account.
|
||||||
|
const filteredAttributeOptions = useMemo( () => {
|
||||||
|
const options = [];
|
||||||
|
|
||||||
|
attributeNames.forEach( ( attributeName ) => {
|
||||||
|
const currentAttribute = attributes[ attributeName ];
|
||||||
|
const attributeNamesExcludingCurrentAttribute = attributeNames.filter(
|
||||||
|
( name ) => name !== attributeName
|
||||||
|
);
|
||||||
|
const matchingVariationIds = hasSelectedAttributes
|
||||||
|
? getVariationsMatchingSelectedAttributes( {
|
||||||
|
selectedAttributes,
|
||||||
|
variationAttributes,
|
||||||
|
attributeNames: attributeNamesExcludingCurrentAttribute,
|
||||||
|
} )
|
||||||
|
: null;
|
||||||
|
const validAttributeTerms =
|
||||||
|
matchingVariationIds !== null
|
||||||
|
? matchingVariationIds.map(
|
||||||
|
( varId ) =>
|
||||||
|
variationAttributes[ varId ][ attributeName ]
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
options[ attributeName ] = getSelectControlOptions(
|
||||||
|
currentAttribute.terms,
|
||||||
|
validAttributeTerms
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}, [
|
||||||
|
attributes,
|
||||||
|
variationAttributes,
|
||||||
|
attributeNames,
|
||||||
|
selectedAttributes,
|
||||||
|
hasSelectedAttributes,
|
||||||
|
] );
|
||||||
|
|
||||||
|
// Select variation when selections change.
|
||||||
|
useEffect( () => {
|
||||||
|
if ( ! hasSelectedAllAttributes ) {
|
||||||
|
setVariationId( 0 );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingVariationIds = getVariationsMatchingSelectedAttributes( {
|
||||||
|
selectedAttributes,
|
||||||
|
variationAttributes,
|
||||||
|
attributeNames,
|
||||||
|
} );
|
||||||
|
|
||||||
|
setVariationId( matchingVariationIds[ 0 ] || 0 );
|
||||||
|
}, [
|
||||||
|
selectedAttributes,
|
||||||
|
variationAttributes,
|
||||||
|
attributeNames,
|
||||||
|
hasSelectedAllAttributes,
|
||||||
|
] );
|
||||||
|
|
||||||
|
// @todo Hook up Variation Picker with Cart Form.
|
||||||
|
return (
|
||||||
|
<div className="wc-block-components-product-add-to-cart-attribute-picker">
|
||||||
|
{ attributeNames.map( ( attributeName ) => (
|
||||||
|
<AttributeSelectControl
|
||||||
|
key={ attributeName }
|
||||||
|
attributeName={ attributeName }
|
||||||
|
options={ filteredAttributeOptions[ attributeName ] }
|
||||||
|
selected={ selectedAttributes[ attributeName ] }
|
||||||
|
onChange={ ( selected ) => {
|
||||||
|
setSelectedAttributes( {
|
||||||
|
...selectedAttributes,
|
||||||
|
[ attributeName ]: selected,
|
||||||
|
} );
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
) ) }
|
||||||
|
<p>Matched variation ID: { variationId }</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AttributePicker;
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { decodeEntities } from '@wordpress/html-entities';
|
||||||
|
import { SelectControl } from '@wordpress/components';
|
||||||
|
|
||||||
|
// Default option for select boxes.
|
||||||
|
const selectAnOption = {
|
||||||
|
value: '',
|
||||||
|
label: __( 'Select an option', 'woo-gutenberg-products-block' ),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VariationAttributeSelect component.
|
||||||
|
*
|
||||||
|
* @param {*} props Component props.
|
||||||
|
*/
|
||||||
|
const AttributeSelectControl = ( {
|
||||||
|
attributeName,
|
||||||
|
options = [],
|
||||||
|
selected = '',
|
||||||
|
onChange = () => {},
|
||||||
|
} ) => {
|
||||||
|
return (
|
||||||
|
<SelectControl
|
||||||
|
className="wc-block-components-product-add-to-cart-attribute-picker__select"
|
||||||
|
label={ decodeEntities( attributeName ) }
|
||||||
|
value={ selected || '' }
|
||||||
|
options={ [ selectAnOption, ...options ] }
|
||||||
|
onChange={ onChange }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AttributeSelectControl;
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import './style.scss';
|
||||||
|
import AttributePicker from './attribute-picker';
|
||||||
|
import { getAttributes, getVariationAttributes } from './utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VariationAttributes component.
|
||||||
|
*
|
||||||
|
* @param {*} props Component props.
|
||||||
|
*/
|
||||||
|
const VariationAttributes = ( { product } ) => {
|
||||||
|
const {
|
||||||
|
attributes: productAttributes = {},
|
||||||
|
variations: productVariations = [],
|
||||||
|
} = product;
|
||||||
|
|
||||||
|
const attributes = getAttributes( productAttributes );
|
||||||
|
const variationAttributes = getVariationAttributes( productVariations );
|
||||||
|
|
||||||
|
if (
|
||||||
|
Object.keys( attributes ).length === 0 ||
|
||||||
|
variationAttributes.length === 0
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AttributePicker
|
||||||
|
attributes={ attributes }
|
||||||
|
variationAttributes={ variationAttributes }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VariationAttributes;
|
|
@ -0,0 +1,18 @@
|
||||||
|
.wc-block-components-product-add-to-cart-attribute-picker {
|
||||||
|
margin: 0;
|
||||||
|
flex-basis: 100%;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
@include font-size(regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-block-components-product-add-to-cart-attribute-picker__select {
|
||||||
|
margin: 0 0 em($gap-small) 0;
|
||||||
|
|
||||||
|
select {
|
||||||
|
min-width: 60%;
|
||||||
|
min-height: 1.75em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { keyBy } from 'lodash';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key an array of attributes by name,
|
||||||
|
*
|
||||||
|
* @param {Object} attributes Attributes array.
|
||||||
|
*/
|
||||||
|
export const getAttributes = ( attributes ) => {
|
||||||
|
return attributes
|
||||||
|
? keyBy(
|
||||||
|
Object.values( attributes ).filter(
|
||||||
|
( { has_variations: hasVariations } ) => hasVariations
|
||||||
|
),
|
||||||
|
'name'
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format variations from the API into a map of just the attribute names and values.
|
||||||
|
*
|
||||||
|
* @param {Array} variations Variations array.
|
||||||
|
*/
|
||||||
|
export const getVariationAttributes = ( variations ) => {
|
||||||
|
if ( ! variations ) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributesMap = {};
|
||||||
|
|
||||||
|
variations.forEach( ( { id, attributes } ) => {
|
||||||
|
attributesMap[ id ] = attributes.reduce( ( acc, { name, value } ) => {
|
||||||
|
acc[ name ] = value;
|
||||||
|
return acc;
|
||||||
|
}, [] );
|
||||||
|
} );
|
||||||
|
|
||||||
|
return attributesMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a list of terms, filter them and return options for the select boxes.
|
||||||
|
*
|
||||||
|
* @param {Object} attributeTerms List of attribute term objects.
|
||||||
|
* @param {?Array} validAttributeTerms Valid values if selections have been made already.
|
||||||
|
* @return {Array} Value/Label pairs of select box options.
|
||||||
|
*/
|
||||||
|
export const getSelectControlOptions = (
|
||||||
|
attributeTerms,
|
||||||
|
validAttributeTerms = null
|
||||||
|
) => {
|
||||||
|
return Object.values( attributeTerms )
|
||||||
|
.map( ( { name, slug } ) => {
|
||||||
|
if (
|
||||||
|
validAttributeTerms === null ||
|
||||||
|
validAttributeTerms.includes( null ) ||
|
||||||
|
validAttributeTerms.includes( slug )
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
value: slug,
|
||||||
|
label: name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} )
|
||||||
|
.filter( Boolean );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a list of variations and a list of attribute values, return variations which match.
|
||||||
|
*
|
||||||
|
* Allows an attribute to be excluded by name. This is used to filter displayed options for
|
||||||
|
* individual attribute selects.
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Object} props.selectedAttributes List of selected attributes.
|
||||||
|
* @param {Object} props.variationAttributes List of variations and their attributes.
|
||||||
|
* @param {Object} props.attributeNames List of all possible attribute names.
|
||||||
|
* @return {Array} List of matching variation IDs.
|
||||||
|
*/
|
||||||
|
export const getVariationsMatchingSelectedAttributes = ( {
|
||||||
|
selectedAttributes,
|
||||||
|
variationAttributes,
|
||||||
|
attributeNames,
|
||||||
|
} ) => {
|
||||||
|
return Object.keys( variationAttributes ).filter( ( variationId ) =>
|
||||||
|
attributeNames.every( ( attributeName ) => {
|
||||||
|
const selectedAttribute = selectedAttributes[ attributeName ] || '';
|
||||||
|
const variationAttribute =
|
||||||
|
variationAttributes[ variationId ][ attributeName ];
|
||||||
|
|
||||||
|
// If there is no selected attribute, consider this a match.
|
||||||
|
if ( selectedAttribute === '' ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// If the variation attributes for this attribute are set to null, it matches all values.
|
||||||
|
if ( variationAttribute === null ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Otherwise, only match if the selected values are the same.
|
||||||
|
return variationAttribute === selectedAttribute;
|
||||||
|
} )
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,14 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { Placeholder } from '@wordpress/components';
|
|
||||||
|
|
||||||
const VariationPicker = () => {
|
|
||||||
return (
|
|
||||||
<Placeholder className="wc-block-components-product-add-to-cart-variation-picker">
|
|
||||||
This is a placeholder for the variation picker form element.
|
|
||||||
</Placeholder>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default VariationPicker;
|
|
|
@ -4,14 +4,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.wc-block-components-product-add-to-cart-variation-picker,
|
|
||||||
.wc-block-components-product-add-to-cart-group-list {
|
|
||||||
margin: 0 0 em($gap-small) 0;
|
|
||||||
flex-basis: 100%;
|
|
||||||
border: 1px solid #000;
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wc-block-components-product-add-to-cart-button {
|
.wc-block-components-product-add-to-cart-button {
|
||||||
margin: 0 0 em($gap-small) 0;
|
margin: 0 0 em($gap-small) 0;
|
||||||
|
|
||||||
|
|
|
@ -19,10 +19,13 @@ jest.mock( '../../base/utils/errors', () => ( {
|
||||||
} ) );
|
} ) );
|
||||||
|
|
||||||
const mockProducts = [
|
const mockProducts = [
|
||||||
{ id: 1, name: 'Hoodie', variations: [ 3, 4 ] },
|
{ id: 1, name: 'Hoodie', variations: [ { id: 3 }, { id: 4 } ] },
|
||||||
{ id: 2, name: 'Backpack' },
|
{ id: 2, name: 'Backpack' },
|
||||||
];
|
];
|
||||||
const mockVariations = [ { id: 3, name: 'Blue' }, { id: 4, name: 'Red' } ];
|
const mockVariations = [
|
||||||
|
{ id: 3, name: 'Blue' },
|
||||||
|
{ id: 4, name: 'Red' },
|
||||||
|
];
|
||||||
const TestComponent = withProductVariations( ( props ) => {
|
const TestComponent = withProductVariations( ( props ) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -88,7 +88,7 @@ const withAttributes = ( OriginalComponent ) => {
|
||||||
newTerms = newTerms.map( ( term ) => ( {
|
newTerms = newTerms.map( ( term ) => ( {
|
||||||
...term,
|
...term,
|
||||||
parent: expandedAttribute,
|
parent: expandedAttribute,
|
||||||
attr_slug: attributeData.slug,
|
attr_slug: attributeData.taxonomy,
|
||||||
} ) );
|
} ) );
|
||||||
|
|
||||||
setTermsList( {
|
setTermsList( {
|
||||||
|
|
|
@ -121,7 +121,8 @@ const withProductVariations = createHigherOrderComponent(
|
||||||
const { products } = this.props;
|
const { products } = this.props;
|
||||||
const parentProduct = products.filter(
|
const parentProduct = products.filter(
|
||||||
( p ) =>
|
( p ) =>
|
||||||
p.variations && p.variations.includes( variationId )
|
p.variations &&
|
||||||
|
p.variations.find( ( { id } ) => id === variationId )
|
||||||
);
|
);
|
||||||
return parentProduct[ 0 ].id;
|
return parentProduct[ 0 ].id;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,8 +41,8 @@ class ProductAttributeSchema extends AbstractSchema {
|
||||||
'context' => array( 'view', 'edit' ),
|
'context' => array( 'view', 'edit' ),
|
||||||
'readonly' => true,
|
'readonly' => true,
|
||||||
),
|
),
|
||||||
'slug' => array(
|
'taxonomy' => array(
|
||||||
'description' => __( 'String based identifier for the attribute, and its WordPress taxonomy.', 'woo-gutenberg-products-block' ),
|
'description' => __( 'The attribute taxonomy name.', 'woo-gutenberg-products-block' ),
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
'context' => array( 'view', 'edit' ),
|
'context' => array( 'view', 'edit' ),
|
||||||
'readonly' => true,
|
'readonly' => true,
|
||||||
|
@ -84,7 +84,7 @@ class ProductAttributeSchema extends AbstractSchema {
|
||||||
return [
|
return [
|
||||||
'id' => (int) $attribute->id,
|
'id' => (int) $attribute->id,
|
||||||
'name' => $this->prepare_html_response( $attribute->name ),
|
'name' => $this->prepare_html_response( $attribute->name ),
|
||||||
'slug' => $attribute->slug,
|
'taxonomy' => $attribute->slug,
|
||||||
'type' => $attribute->type,
|
'type' => $attribute->type,
|
||||||
'order' => $attribute->order_by,
|
'order' => $attribute->order_by,
|
||||||
'has_archives' => $attribute->has_archives,
|
'has_archives' => $attribute->has_archives,
|
||||||
|
|
|
@ -177,12 +177,6 @@ class ProductSchema extends AbstractSchema {
|
||||||
'properties' => $this->image_attachment_schema->get_properties(),
|
'properties' => $this->image_attachment_schema->get_properties(),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'variations' => [
|
|
||||||
'description' => __( 'List of variation IDs, if applicable.', 'woo-gutenberg-products-block' ),
|
|
||||||
'type' => 'array',
|
|
||||||
'context' => [ 'view', 'edit' ],
|
|
||||||
'items' => 'number',
|
|
||||||
],
|
|
||||||
'categories' => [
|
'categories' => [
|
||||||
'description' => __( 'List of categories, if applicable.', 'woo-gutenberg-products-block' ),
|
'description' => __( 'List of categories, if applicable.', 'woo-gutenberg-products-block' ),
|
||||||
'type' => 'array',
|
'type' => 'array',
|
||||||
|
@ -251,8 +245,108 @@ class ProductSchema extends AbstractSchema {
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'attributes' => [
|
||||||
|
'description' => __( 'List of attributes assigned to the product/variation that are visible or used for variations.', 'woo-gutenberg-products-block' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'context' => [ 'view', 'edit' ],
|
||||||
|
'items' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'id' => [
|
||||||
|
'description' => __( 'The attribute ID, or 0 if the attribute is not taxonomy based.', 'woo-gutenberg-products-block' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'context' => [ 'view', 'edit' ],
|
||||||
|
'readonly' => true,
|
||||||
|
],
|
||||||
|
'name' => [
|
||||||
|
'description' => __( 'The attribute name.', 'woo-gutenberg-products-block' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'context' => [ 'view', 'edit' ],
|
||||||
|
'readonly' => true,
|
||||||
|
],
|
||||||
|
'taxonomy' => [
|
||||||
|
'description' => __( 'The attribute taxonomy, or null if the attribute is not taxonomy based.', 'woo-gutenberg-products-block' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'context' => [ 'view', 'edit' ],
|
||||||
|
'readonly' => true,
|
||||||
|
],
|
||||||
|
'has_variations' => [
|
||||||
|
'description' => __( 'True if this attribute is used by product variations.', 'woo-gutenberg-products-block' ),
|
||||||
|
'type' => 'boolean',
|
||||||
|
'context' => [ 'view', 'edit' ],
|
||||||
|
'readonly' => true,
|
||||||
|
],
|
||||||
|
'terms' => [
|
||||||
|
'description' => __( 'List of assigned attribute terms.', 'woo-gutenberg-products-block' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'context' => [ 'view', 'edit' ],
|
||||||
|
'items' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'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' => [
|
||||||
|
'description' => __( 'The term name.', 'woo-gutenberg-products-block' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'context' => [ 'view', 'edit' ],
|
||||||
|
'readonly' => true,
|
||||||
|
],
|
||||||
|
'slug' => [
|
||||||
|
'description' => __( 'The term slug.', 'woo-gutenberg-products-block' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'context' => [ 'view', 'edit' ],
|
||||||
|
'readonly' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'variations' => [
|
||||||
|
'description' => __( 'List of variation IDs, if applicable.', 'woo-gutenberg-products-block' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'context' => [ 'view', 'edit' ],
|
||||||
|
'items' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'id' => [
|
||||||
|
'description' => __( 'The attribute ID, or 0 if the attribute is not taxonomy based.', 'woo-gutenberg-products-block' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'context' => [ 'view', 'edit' ],
|
||||||
|
'readonly' => true,
|
||||||
|
],
|
||||||
|
'attributes' => [
|
||||||
|
'description' => __( 'List of variation attributes.', 'woo-gutenberg-products-block' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'context' => [ 'view', 'edit' ],
|
||||||
|
'items' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'name' => [
|
||||||
|
'description' => __( 'The attribute name.', 'woo-gutenberg-products-block' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'context' => [ 'view', 'edit' ],
|
||||||
|
'readonly' => true,
|
||||||
|
],
|
||||||
|
'value' => [
|
||||||
|
'description' => __( 'The assigned attribute.', 'woo-gutenberg-products-block' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'context' => [ 'view', 'edit' ],
|
||||||
|
'readonly' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
'has_options' => [
|
'has_options' => [
|
||||||
'description' => __( 'Does the product have options?', 'woo-gutenberg-products-block' ),
|
'description' => __( 'Does the product have additional options before it can be added to the cart?', 'woo-gutenberg-products-block' ),
|
||||||
'type' => 'boolean',
|
'type' => 'boolean',
|
||||||
'context' => [ 'view', 'edit' ],
|
'context' => [ 'view', 'edit' ],
|
||||||
'readonly' => true,
|
'readonly' => true,
|
||||||
|
@ -345,9 +439,10 @@ class ProductSchema extends AbstractSchema {
|
||||||
'average_rating' => $product->get_average_rating(),
|
'average_rating' => $product->get_average_rating(),
|
||||||
'review_count' => $product->get_review_count(),
|
'review_count' => $product->get_review_count(),
|
||||||
'images' => $this->get_images( $product ),
|
'images' => $this->get_images( $product ),
|
||||||
'variations' => $product->is_type( 'variable' ) ? $product->get_visible_children() : [],
|
|
||||||
'categories' => $this->get_term_list( $product, 'product_cat' ),
|
'categories' => $this->get_term_list( $product, 'product_cat' ),
|
||||||
'tags' => $this->get_term_list( $product, 'product_tag' ),
|
'tags' => $this->get_term_list( $product, 'product_tag' ),
|
||||||
|
'attributes' => $this->get_attributes( $product ),
|
||||||
|
'variations' => $this->get_variations( $product ),
|
||||||
'has_options' => $product->has_options(),
|
'has_options' => $product->has_options(),
|
||||||
'is_purchasable' => $product->is_purchasable(),
|
'is_purchasable' => $product->is_purchasable(),
|
||||||
'is_in_stock' => $product->is_in_stock(),
|
'is_in_stock' => $product->is_in_stock(),
|
||||||
|
@ -424,6 +519,146 @@ class ProductSchema extends AbstractSchema {
|
||||||
return apply_filters( 'woocommerce_store_api_product_quantity_limit', max( min( array_filter( $limits ) ), 1 ), $product );
|
return apply_filters( 'woocommerce_store_api_product_quantity_limit', max( min( array_filter( $limits ) ), 1 ), $product );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given attribute is valid.
|
||||||
|
*
|
||||||
|
* @param mixed $attribute Object or variable to check.
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
protected function filter_valid_attribute( $attribute ) {
|
||||||
|
return is_a( $attribute, '\WC_Product_Attribute' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given attribute is valid and used for variations.
|
||||||
|
*
|
||||||
|
* @param mixed $attribute Object or variable to check.
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
protected function filter_variation_attribute( $attribute ) {
|
||||||
|
return $this->filter_valid_attribute( $attribute ) && $attribute->get_variation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get variation IDs and attributes from the DB.
|
||||||
|
*
|
||||||
|
* @param \WC_Product $product Product instance.
|
||||||
|
* @returns array
|
||||||
|
*/
|
||||||
|
protected function get_variations( \WC_Product $product ) {
|
||||||
|
if ( ! $product->is_type( 'variable' ) ) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$variation_ids = $product->get_visible_children();
|
||||||
|
$attributes = array_filter( $product->get_attributes(), [ $this, 'filter_variation_attribute' ] );
|
||||||
|
$default_variation_meta_data = array_reduce(
|
||||||
|
$attributes,
|
||||||
|
function( $defaults, $attribute ) {
|
||||||
|
$meta_key = wc_variation_attribute_name( $attribute->get_name() );
|
||||||
|
$defaults[ $meta_key ] = [
|
||||||
|
'name' => wc_attribute_label( $attribute->get_name(), $product ),
|
||||||
|
'value' => null,
|
||||||
|
];
|
||||||
|
return $defaults;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
|
||||||
|
$variation_meta_data = $wpdb->get_results(
|
||||||
|
"
|
||||||
|
SELECT post_id as variation_id, meta_key as attribute_key, meta_value as attribute_value
|
||||||
|
FROM {$wpdb->postmeta}
|
||||||
|
WHERE post_id IN (" . implode( ',', array_map( 'esc_sql', $variation_ids ) ) . ")
|
||||||
|
AND meta_key IN ('" . implode( "','", array_map( 'esc_sql', array_keys( $default_variation_meta_data ) ) ) . "')
|
||||||
|
"
|
||||||
|
);
|
||||||
|
// phpcs:enable
|
||||||
|
|
||||||
|
$attributes_by_variation = array_reduce(
|
||||||
|
$variation_meta_data,
|
||||||
|
function( $values, $data ) {
|
||||||
|
$values[ $data->variation_id ][ $data->attribute_key ] = $data->attribute_value;
|
||||||
|
return $values;
|
||||||
|
},
|
||||||
|
array_fill_keys( $variation_ids, [] )
|
||||||
|
);
|
||||||
|
|
||||||
|
$variations = [];
|
||||||
|
|
||||||
|
foreach ( $variation_ids as $variation_id ) {
|
||||||
|
$attribute_data = $default_variation_meta_data;
|
||||||
|
|
||||||
|
foreach ( $attributes_by_variation[ $variation_id ] as $meta_key => $meta_value ) {
|
||||||
|
if ( '' !== $meta_value ) {
|
||||||
|
$attribute_data[ $meta_key ]['value'] = $meta_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$variations[] = (object) [
|
||||||
|
'id' => $variation_id,
|
||||||
|
'attributes' => array_values( $attribute_data ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $variations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of product attributes and attribute terms.
|
||||||
|
*
|
||||||
|
* @param \WC_Product $product Product instance.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function get_attributes( \WC_Product $product ) {
|
||||||
|
$attributes = array_filter( $product->get_attributes(), [ $this, 'filter_valid_attribute' ] );
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
$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() ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare an attribute term for the response.
|
||||||
|
*
|
||||||
|
* @param \WP_Term $term Term object.
|
||||||
|
* @return object
|
||||||
|
*/
|
||||||
|
protected function prepare_product_attribute_taxonomy_value( \WP_Term $term ) {
|
||||||
|
return $this->prepare_product_attribute_value( $term->name, $term->term_id, $term->slug );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare an attribute term for the response.
|
||||||
|
*
|
||||||
|
* @param string $name Attribute term name.
|
||||||
|
* @param int $id Attribute term ID.
|
||||||
|
* @param string $slug Attribute term slug.
|
||||||
|
* @return object
|
||||||
|
*/
|
||||||
|
protected function prepare_product_attribute_value( $name, $id = 0, $slug = '' ) {
|
||||||
|
return (object) [
|
||||||
|
'id' => (int) $id,
|
||||||
|
'name' => $name,
|
||||||
|
'slug' => $slug ? $slug : $name,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an array of pricing data.
|
* Get an array of pricing data.
|
||||||
*
|
*
|
||||||
|
|
|
@ -51,7 +51,7 @@ class ProductAttributes extends TestCase {
|
||||||
$this->assertEquals( 200, $response->get_status() );
|
$this->assertEquals( 200, $response->get_status() );
|
||||||
$this->assertEquals( $this->attributes[0]->id, $data['id'] );
|
$this->assertEquals( $this->attributes[0]->id, $data['id'] );
|
||||||
$this->assertEquals( $this->attributes[0]->name, $data['name'] );
|
$this->assertEquals( $this->attributes[0]->name, $data['name'] );
|
||||||
$this->assertEquals( $this->attributes[0]->slug, $data['slug'] );
|
$this->assertEquals( $this->attributes[0]->slug, $data['taxonomy'] );
|
||||||
$this->assertEquals( $this->attributes[0]->type, $data['type'] );
|
$this->assertEquals( $this->attributes[0]->type, $data['type'] );
|
||||||
$this->assertEquals( $this->attributes[0]->order_by, $data['order'] );
|
$this->assertEquals( $this->attributes[0]->order_by, $data['order'] );
|
||||||
$this->assertEquals( $this->attributes[0]->has_archives, $data['has_archives'] );
|
$this->assertEquals( $this->attributes[0]->has_archives, $data['has_archives'] );
|
||||||
|
@ -68,7 +68,7 @@ class ProductAttributes extends TestCase {
|
||||||
$this->assertEquals( 2, count( $data ) );
|
$this->assertEquals( 2, count( $data ) );
|
||||||
$this->assertArrayHasKey( 'id', $data[0] );
|
$this->assertArrayHasKey( 'id', $data[0] );
|
||||||
$this->assertArrayHasKey( 'name', $data[0] );
|
$this->assertArrayHasKey( 'name', $data[0] );
|
||||||
$this->assertArrayHasKey( 'slug', $data[0] );
|
$this->assertArrayHasKey( 'taxonomy', $data[0] );
|
||||||
$this->assertArrayHasKey( 'type', $data[0] );
|
$this->assertArrayHasKey( 'type', $data[0] );
|
||||||
$this->assertArrayHasKey( 'order', $data[0] );
|
$this->assertArrayHasKey( 'order', $data[0] );
|
||||||
$this->assertArrayHasKey( 'has_archives', $data[0] );
|
$this->assertArrayHasKey( 'has_archives', $data[0] );
|
||||||
|
@ -85,7 +85,7 @@ class ProductAttributes extends TestCase {
|
||||||
|
|
||||||
$this->assertArrayHasKey( 'id', $data );
|
$this->assertArrayHasKey( 'id', $data );
|
||||||
$this->assertArrayHasKey( 'name', $data );
|
$this->assertArrayHasKey( 'name', $data );
|
||||||
$this->assertArrayHasKey( 'slug', $data );
|
$this->assertArrayHasKey( 'taxonomy', $data );
|
||||||
$this->assertArrayHasKey( 'type', $data );
|
$this->assertArrayHasKey( 'type', $data );
|
||||||
$this->assertArrayHasKey( 'order', $data );
|
$this->assertArrayHasKey( 'order', $data );
|
||||||
$this->assertArrayHasKey( 'has_archives', $data );
|
$this->assertArrayHasKey( 'has_archives', $data );
|
||||||
|
|
Loading…
Reference in New Issue