Followup: switch namespace on useStoreProducts hook. (https://github.com/woocommerce/woocommerce-blocks/pull/1102)

* add missing `add_to_cart` properties to product schema

Also camelcase properties.

* switch namespace to `/wc/store/`

* add experimental action for perisisting and item to a given collection

* refactor ProductButton to use hooks (initial pass)

This is just the initial refactor to figure out the logic.  I’m going to do another pass to see about extracting some of this to a custom hook (because it’s kind of gnarly to have to repeat… and it’s possible it can be simplified as well).

* add new properties to tests and ensure test is using the same product instance values as the rest request

* refactor to add custom internal only useAddToCart hook.

* fix value extraction from product object

* revert casing changes
This commit is contained in:
Darren Ethier 2019-11-04 04:47:14 -06:00 committed by Mike Jolley
parent 9fd3ab7e74
commit a7beb3b845
3 changed files with 188 additions and 108 deletions

View File

@ -3,129 +3,162 @@
*/
import PropTypes from 'prop-types';
import classnames from 'classnames';
import apiFetch from '@wordpress/api-fetch';
import { __, sprintf } from '@wordpress/i18n';
import { Component } from 'react';
import { addQueryArgs } from '@wordpress/url';
import {
useMemo,
useCallback,
useState,
useEffect,
useRef,
} from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { find } from 'lodash';
class ProductButton extends Component {
static propTypes = {
className: PropTypes.string,
product: PropTypes.object.isRequired,
/**
* Internal dependencies
*/
import { useCollection } from '@woocommerce/base-hooks';
import { COLLECTIONS_STORE_KEY as storeKey } from '@woocommerce/block-data';
/**
* A custom hook for exposing cart related data for a given product id and an
* action for adding a single quantity of the product _to_ the cart.
*
* Currently this is internal only to the ProductButton component until we have
* a clearer idea of the pattern that should emerge for a cart hook.
*
* @param {number} productId The product id for the product connection to the
* cart.
*
* @return {Object} Returns an object with the following properties:
* @type {number} cartQuantity The quantity of the product currently in
* the cart.
* @type {bool} addingToCart Whether the product is currently being
* added to the cart (true).
* @type {bool} cartIsLoading Whether the cart is being loaded.
* @type {function} addToCart An action dispatcher for adding a single
* quantity of the product to the cart.
* Receives no arguments, it operates on the
* current product.
*/
const useAddToCart = ( productId ) => {
const { results: cartResults, isLoading: cartIsLoading } = useCollection( {
namespace: '/wc/store',
resourceName: 'cart/items',
} );
const currentCartResults = useRef( null );
const { __experimentalPersistItemToCollection } = useDispatch( storeKey );
const cartQuantity = useMemo( () => {
const productItem = find( cartResults, { id: productId } );
return productItem ? productItem.quantity : 0;
}, [ cartResults, productId ] );
const [ addingToCart, setAddingToCart ] = useState( false );
const addToCart = useCallback( () => {
setAddingToCart( true );
// exclude this item from the cartResults for adding to the new
// collection (so it's updated correctly!)
const collection = cartResults.filter( ( cartItem ) => {
return cartItem.id !== productId;
} );
__experimentalPersistItemToCollection(
'/wc/store',
'cart/items',
collection,
{ id: productId, quantity: 1 }
);
}, [ productId, cartResults ] );
useEffect( () => {
if ( currentCartResults.current !== cartResults ) {
if ( addingToCart ) {
setAddingToCart( false );
}
currentCartResults.current = cartResults;
}
}, [ cartResults, addingToCart ] );
return {
cartQuantity,
addingToCart,
cartIsLoading,
addToCart,
};
};
state = {
addedToCart: false,
addingToCart: false,
cartQuantity: null,
};
onAddToCart = () => {
const { product } = this.props;
this.setState( { addingToCart: true } );
return apiFetch( {
method: 'POST',
path: '/wc/blocks/cart/add',
data: {
product_id: product.id,
quantity: 1,
},
cache: 'no-store',
} )
.then( ( response ) => {
const newQuantity = response.quantity;
this.setState( {
addedToCart: true,
addingToCart: false,
cartQuantity: newQuantity,
} );
} )
.catch( ( response ) => {
if ( response.code ) {
return ( document.location.href = addQueryArgs(
product.permalink,
{ wc_error: response.message }
) );
}
document.location.href = product.permalink;
} );
};
getButtonText = () => {
const { product } = this.props;
const { cartQuantity } = this.state;
if ( Number.isFinite( cartQuantity ) ) {
const ProductButton = ( { product, className } ) => {
const {
id,
permalink,
add_to_cart: productCartDetails,
has_options: hasOptions,
is_purchasable: isPurchasable,
is_in_stock: isInStock,
} = product;
const {
cartQuantity,
addingToCart,
cartIsLoading,
addToCart,
} = useAddToCart( id );
const addedToCart = cartQuantity > 0;
const getButtonText = () => {
if ( Number.isFinite( cartQuantity ) && addedToCart ) {
return sprintf(
__( '%d in cart', 'woo-gutenberg-products-block' ),
cartQuantity
);
}
return product.add_to_cart.text;
return productCartDetails.text;
};
const wrapperClasses = classnames(
className,
'wc-block-grid__product-add-to-cart',
'wp-block-button'
);
render = () => {
const { product, className } = this.props;
const { addingToCart, addedToCart } = this.state;
const wrapperClasses = classnames(
className,
'wc-block-grid__product-add-to-cart',
'wp-block-button'
);
const buttonClasses = classnames(
'wp-block-button__link',
'add_to_cart_button',
{
loading: addingToCart,
added: addedToCart,
}
);
if ( Object.keys( product ).length === 0 ) {
return (
<div className={ wrapperClasses }>
<button className={ buttonClasses } disabled={ true } />
</div>
);
const buttonClasses = classnames(
'wp-block-button__link',
'add_to_cart_button',
{
loading: addingToCart,
added: addedToCart,
}
);
const allowAddToCart =
! product.has_options &&
product.is_purchasable &&
product.is_in_stock;
const buttonText = this.getButtonText();
if ( Object.keys( product ).length === 0 || cartIsLoading ) {
return (
<div className={ wrapperClasses }>
{ allowAddToCart ? (
<button
onClick={ this.onAddToCart }
aria-label={ product.add_to_cart.description }
className={ buttonClasses }
disabled={ addingToCart }
>
{ buttonText }
</button>
) : (
<a
href={ product.permalink }
aria-label={ product.add_to_cart.description }
className={ buttonClasses }
rel="nofollow"
>
{ buttonText }
</a>
) }
<button className={ buttonClasses } disabled={ true } />
</div>
);
};
}
}
const allowAddToCart = ! hasOptions && isPurchasable && isInStock;
return (
<div className={ wrapperClasses }>
{ allowAddToCart ? (
<button
onClick={ addToCart }
aria-label={ productCartDetails.description }
className={ buttonClasses }
disabled={ addingToCart }
>
{ getButtonText() }
</button>
) : (
<a
href={ permalink }
aria-label={ productCartDetails.description }
className={ buttonClasses }
rel="nofollow"
>
{ getButtonText() }
</a>
) }
</div>
);
};
ProductButton.propTypes = {
className: PropTypes.string,
product: PropTypes.object.isRequired,
};
export default ProductButton;

View File

@ -25,7 +25,7 @@ export const useStoreProducts = ( query ) => {
// @todo see @https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/1097
// where the namespace is going to be changed. Not doing in this pull.
const collectionOptions = {
namespace: '/wc/blocks',
namespace: '/wc/store',
resourceName: 'products',
query,
};

View File

@ -1,4 +1,13 @@
/**
* External dependencies
*/
import { apiFetch, select } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import { ACTION_TYPES as types } from './action-types';
import { STORE_KEY as SCHEMA_STORE_KEY } from '../schema/constants';
let Headers = window.Headers || null;
Headers = Headers
@ -53,3 +62,41 @@ export function receiveCollection(
response,
};
}
export function* __experimentalPersistItemToCollection(
namespace,
resourceName,
currentCollection,
data = {}
) {
const newCollection = [ ...currentCollection ];
const route = yield select(
SCHEMA_STORE_KEY,
'getRoute',
namespace,
resourceName
);
if ( ! route ) {
return;
}
const item = yield apiFetch( {
path: route,
method: 'POST',
data,
cache: 'no-store',
} );
if ( item ) {
newCollection.push( item );
yield receiveCollection(
namespace,
resourceName,
'',
[],
{
items: newCollection,
headers: Headers,
},
true
);
}
}