diff --git a/plugins/woocommerce-blocks/assets/js/base/hooks/index.js b/plugins/woocommerce-blocks/assets/js/base/hooks/index.js index 878f82391d3..63c2e42b535 100644 --- a/plugins/woocommerce-blocks/assets/js/base/hooks/index.js +++ b/plugins/woocommerce-blocks/assets/js/base/hooks/index.js @@ -2,6 +2,7 @@ export * from './use-query-state'; export * from './use-shallow-equal'; export * from './use-store-cart'; export * from './use-store-cart-coupons'; +export * from './use-store-cart-item'; export * from './use-store-products'; export * from './use-collection'; export * from './use-collection-header'; diff --git a/plugins/woocommerce-blocks/assets/js/base/hooks/use-store-cart-item.js b/plugins/woocommerce-blocks/assets/js/base/hooks/use-store-cart-item.js new file mode 100644 index 00000000000..3dedd929222 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/base/hooks/use-store-cart-item.js @@ -0,0 +1,47 @@ +/** @typedef { import('@woocommerce/type-defs/hooks').StoreCartItems } StoreCartItems */ + +/** + * External dependencies + */ +import { useSelect } from '@wordpress/data'; +import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data'; + +/** + * Internal dependencies + */ +import { useStoreCart } from './use-store-cart'; + +/** + * This is a custom hook for loading the Store API /cart/ endpoint and + * actions for removing or changing item quantity. + * See also: https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/master/src/RestApi/StoreApi + * + * @param {string} cartItemKey Key for a cart item. + * @return {StoreCartItems} An object exposing data and actions relating to cart items. + */ +export const useStoreCartItem = ( cartItemKey ) => { + const { cartItems, cartIsLoading } = useStoreCart(); + const cartItem = cartItems.filter( ( item ) => item.key === cartItemKey ); + + const results = useSelect( + ( select, { dispatch } ) => { + const store = select( storeKey ); + const isPending = store.isItemQuantityPending( cartItemKey ); + const { removeItemFromCart } = dispatch( storeKey ); + + return { + isPending, + removeItem: () => { + removeItemFromCart( cartItemKey ); + }, + }; + }, + [ cartItemKey ] + ); + + return { + isLoading: cartIsLoading, + cartItem, + ...results, + }; +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout/cart/full-cart/cart-line-item-row.js b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout/cart/full-cart/cart-line-item-row.js index e440661743f..28887b2960f 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout/cart/full-cart/cart-line-item-row.js +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout/cart/full-cart/cart-line-item-row.js @@ -7,6 +7,7 @@ import PropTypes from 'prop-types'; import QuantitySelector from '@woocommerce/base-components/quantity-selector'; import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount'; import { getCurrency, formatPrice } from '@woocommerce/base-utils'; +import { useStoreCartItem } from '@woocommerce/base-hooks'; import { Icon, trash } from '@woocommerce/icons'; /** @@ -21,6 +22,7 @@ import ProductLowStockBadge from './product-low-stock-badge'; */ const CartLineItemRow = ( { lineItem = {} } ) => { const { + key = '', name = '', summary = '', permalink = '', @@ -36,6 +38,10 @@ const CartLineItemRow = ( { lineItem = {} } ) => { const purchasePrice = parseInt( prices.price, 10 ) * lineQuantity; const saleAmount = regularPrice - purchasePrice; + const { removeItem, isPending: itemQuantityDisabled } = useStoreCartItem( + key + ); + return ( @@ -60,14 +66,23 @@ const CartLineItemRow = ( { lineItem = {} } ) => { - - @@ -100,6 +115,7 @@ const CartLineItemRow = ( { lineItem = {} } ) => { CartLineItemRow.propTypes = { lineItem: PropTypes.shape( { + key: PropTypes.string.isRequired, name: PropTypes.string.isRequired, summary: PropTypes.string.isRequired, images: PropTypes.array.isRequired, diff --git a/plugins/woocommerce-blocks/assets/js/data/cart/action-types.js b/plugins/woocommerce-blocks/assets/js/data/cart/action-types.js index f448e01c676..8b5c7a749dd 100644 --- a/plugins/woocommerce-blocks/assets/js/data/cart/action-types.js +++ b/plugins/woocommerce-blocks/assets/js/data/cart/action-types.js @@ -4,4 +4,6 @@ export const ACTION_TYPES = { REPLACE_ERRORS: 'REPLACE_ERRORS', APPLYING_COUPON: 'APPLYING_COUPON', REMOVING_COUPON: 'REMOVING_COUPON', + ITEM_QUANTITY_PENDING: 'ITEM_QUANTITY_PENDING', + RECEIVE_REMOVED_ITEM: 'RECEIVE_REMOVED_ITEM', }; diff --git a/plugins/woocommerce-blocks/assets/js/data/cart/actions.js b/plugins/woocommerce-blocks/assets/js/data/cart/actions.js index f1045b4ffc8..e970b41d1eb 100644 --- a/plugins/woocommerce-blocks/assets/js/data/cart/actions.js +++ b/plugins/woocommerce-blocks/assets/js/data/cart/actions.js @@ -125,3 +125,59 @@ export function* removeCoupon( couponCode ) { yield receiveRemovingCoupon( '' ); } + +/** + * Returns an action object to indicate if the specified cart item + * is being updated; i.e. removing, or changing quantity. + * + * @param {string} cartItemKey Cart item being updated. + * @param {boolean} isQuantityPending Flag for update state; true if API request is pending. + * @return {Object} Object for action. + */ +export function itemQuantityPending( cartItemKey, isQuantityPending ) { + return { + type: types.ITEM_QUANTITY_PENDING, + cartItemKey, + isQuantityPending, + }; +} + +/** + * Returns an action object to remove a cart item from the store. + * + * @param {string} cartItemKey Cart item to remove. + * @return {Object} Object for action. + */ +export function receiveRemovedItem( cartItemKey ) { + return { + type: types.RECEIVE_REMOVED_ITEM, + cartItemKey, + }; +} + +/** + * Removes specified item from the cart: + * - Calls API to remove item. + * - If successful, yields action to remove item from store. + * - If error, yields action to store error. + * - Sets cart item as pending while API request is in progress. + * + * @param {string} cartItemKey Cart item being updated. + */ +export function* removeItemFromCart( cartItemKey ) { + yield itemQuantityPending( cartItemKey, true ); + + try { + yield apiFetch( { + path: `/wc/store/cart/items/${ cartItemKey }`, + method: 'DELETE', + cache: 'no-store', + } ); + + yield receiveRemovedItem( cartItemKey ); + } catch ( error ) { + yield receiveError( error ); + } + + yield itemQuantityPending( cartItemKey, false ); +} diff --git a/plugins/woocommerce-blocks/assets/js/data/cart/reducers.js b/plugins/woocommerce-blocks/assets/js/data/cart/reducers.js index 159b73d57d2..3ac6a3f3e55 100644 --- a/plugins/woocommerce-blocks/assets/js/data/cart/reducers.js +++ b/plugins/woocommerce-blocks/assets/js/data/cart/reducers.js @@ -3,6 +3,22 @@ */ import { ACTION_TYPES as types } from './action-types'; +/** + * Sub-reducer for cart items array. + * + * @param {Array} state cartData.items state slice. + * @param {Object} action Action object. + */ +const cartItemsReducer = ( state = [], action ) => { + switch ( action.type ) { + case types.RECEIVE_REMOVED_ITEM: + return state.filter( ( cartItem ) => { + return cartItem.key !== action.cartItemKey; + } ); + } + return state; +}; + /** * Reducer for receiving items related to the cart. * @@ -13,6 +29,7 @@ import { ACTION_TYPES as types } from './action-types'; */ const reducer = ( state = { + cartItemsQuantityPending: [], cartData: { coupons: [], items: [], @@ -64,6 +81,32 @@ const reducer = ( }, }; break; + + case types.ITEM_QUANTITY_PENDING: + // Remove key by default - handles isQuantityPending==false + // and prevents duplicates when isQuantityPending===true. + const newPendingKeys = state.cartItemsQuantityPending.filter( + ( key ) => key !== action.cartItemKey + ); + if ( action.isQuantityPending ) { + newPendingKeys.push( action.cartItemKey ); + } + state = { + ...state, + cartItemsQuantityPending: newPendingKeys, + }; + break; + + // Delegate to cartItemsReducer. + case types.RECEIVE_REMOVED_ITEM: + state = { + ...state, + cartData: { + ...state.cartData, + items: cartItemsReducer( state.cartData.items, action ), + }, + }; + break; } return state; }; diff --git a/plugins/woocommerce-blocks/assets/js/data/cart/selectors.js b/plugins/woocommerce-blocks/assets/js/data/cart/selectors.js index dce180b9fcd..e904880d9e7 100644 --- a/plugins/woocommerce-blocks/assets/js/data/cart/selectors.js +++ b/plugins/woocommerce-blocks/assets/js/data/cart/selectors.js @@ -106,3 +106,27 @@ export const isRemovingCoupon = ( state ) => { export const getCouponBeingRemoved = ( state ) => { return state.metaData.removingCoupon || ''; }; + +/** + * Returns cart item matching specified key. + * + * @param {Object} state The current state. + * @param {string} cartItemKey Key for a cart item. + * @return {Object} Cart item object, or undefined if not found. + */ +export const getCartItem = ( state, cartItemKey ) => { + return state.cartData.items.find( + ( cartItem ) => cartItem.key === cartItemKey + ); +}; + +/** + * Returns true if the quantity is being updated for the specified cart item. + * + * @param {Object} state The current state. + * @param {string} cartItemKey Key for a cart item. + * @return {boolean} True if a item has a pending request to delete / update quantity. + */ +export const isItemQuantityPending = ( state, cartItemKey ) => { + return state.cartItemsQuantityPending.includes( cartItemKey ); +}; diff --git a/plugins/woocommerce-blocks/assets/js/type-defs/hooks.js b/plugins/woocommerce-blocks/assets/js/type-defs/hooks.js index 3e42b427d0a..28ce7c673d0 100644 --- a/plugins/woocommerce-blocks/assets/js/type-defs/hooks.js +++ b/plugins/woocommerce-blocks/assets/js/type-defs/hooks.js @@ -23,4 +23,15 @@ * @property {boolean} isRemovingCoupon True when a coupon is being removed. */ -export {}; +/** + * @typedef {Object} StoreCartItems + * + * @property {boolean} isLoading True when cart items are being loaded. + * @property {Array} cartItems An array of items in the cart. + * @property {Function} isItemQuantityPending Callback for determining if a cart item + * is currently updating (i.e. remoe / change + * quantity). + * @property {Function} removeItemFromCart Callback for removing a cart item. + */ + + export {};