From a7beb3b845ddfd19fd85f8908f6568f55a0c77d1 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Mon, 4 Nov 2019 04:47:14 -0600 Subject: [PATCH] Followup: switch namespace on useStoreProducts hook. (https://github.com/woocommerce/woocommerce-blocks/pull/1102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../atomic/components/product/button/index.js | 247 ++++++++++-------- .../js/base/hooks/use-store-products.js | 2 +- .../assets/js/data/collections/actions.js | 47 ++++ 3 files changed, 188 insertions(+), 108 deletions(-) diff --git a/plugins/woocommerce-blocks/assets/js/atomic/components/product/button/index.js b/plugins/woocommerce-blocks/assets/js/atomic/components/product/button/index.js index 8419fae9fb5..c322ca59292 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/components/product/button/index.js +++ b/plugins/woocommerce-blocks/assets/js/atomic/components/product/button/index.js @@ -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 ( -
-
- ); + 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 (
- { allowAddToCart ? ( - - ) : ( - - { buttonText } - - ) } +
); - }; -} + } + const allowAddToCart = ! hasOptions && isPurchasable && isInStock; + return ( +
+ { allowAddToCart ? ( + + ) : ( + + { getButtonText() } + + ) } +
+ ); +}; + +ProductButton.propTypes = { + className: PropTypes.string, + product: PropTypes.object.isRequired, +}; export default ProductButton; diff --git a/plugins/woocommerce-blocks/assets/js/base/hooks/use-store-products.js b/plugins/woocommerce-blocks/assets/js/base/hooks/use-store-products.js index dd05fac432e..487fe161c29 100644 --- a/plugins/woocommerce-blocks/assets/js/base/hooks/use-store-products.js +++ b/plugins/woocommerce-blocks/assets/js/base/hooks/use-store-products.js @@ -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, }; diff --git a/plugins/woocommerce-blocks/assets/js/data/collections/actions.js b/plugins/woocommerce-blocks/assets/js/data/collections/actions.js index 5d6a82922c2..8de1829ce16 100644 --- a/plugins/woocommerce-blocks/assets/js/data/collections/actions.js +++ b/plugins/woocommerce-blocks/assets/js/data/collections/actions.js @@ -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 + ); + } +}