support removing cart items (https://github.com/woocommerce/woocommerce-blocks/pull/1813)
* first cut - removing an item from cart: - add actions to cart store for removing an item and keeping track of pending removal API call - add reducer logic for storing pending state on an item, and removing an item - expose removeCartItem on new useStoreCartItems hook - hook it up to remove link / trashcan icon in row item * disable cart quantity picker/remove link while API request in progress: - expose cart item pending status from store using selector - use selector to disable quantity related components in line item row * fix jsdoc - getCartItem returns undefined if not found * add typedef for cart items store object provided by hook * fix rebase error - key prop went awol * orient useStoreCartItem hook to single cart item: - simplify interface for client component - isPending bool (was callback) - removeItem callback no need to specify item key + reinstate disabled prop on remove link when updating (lost in rebase) * move cart item pending state out of cartItems, preserve API state shape: - pending is now stored as array of keys - fix isItemQuantityPending selector (now much simpler) * ensure react knows that our useSelect depends on cartItemKey
This commit is contained in:
parent
bb637bbaa3
commit
69ea94378b
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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 (
|
||||
<tr className="wc-block-cart-items__row">
|
||||
<td className="wc-block-cart-item__image">
|
||||
|
@ -60,14 +66,23 @@ const CartLineItemRow = ( { lineItem = {} } ) => {
|
|||
</td>
|
||||
<td className="wc-block-cart-item__quantity">
|
||||
<QuantitySelector
|
||||
disabled={ itemQuantityDisabled }
|
||||
quantity={ lineQuantity }
|
||||
onChange={ setLineQuantity }
|
||||
itemName={ name }
|
||||
/>
|
||||
<button className="wc-block-cart-item__remove-link">
|
||||
<button
|
||||
disabled={ itemQuantityDisabled }
|
||||
className="wc-block-cart-item__remove-link"
|
||||
onClick={ removeItem }
|
||||
>
|
||||
{ __( 'Remove item', 'woo-gutenberg-products-block' ) }
|
||||
</button>
|
||||
<button className="wc-block-cart-item__remove-icon">
|
||||
<button
|
||||
disabled={ itemQuantityDisabled }
|
||||
className="wc-block-cart-item__remove-icon"
|
||||
onClick={ removeItem }
|
||||
>
|
||||
<Icon srcElement={ trash } />
|
||||
</button>
|
||||
</td>
|
||||
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 );
|
||||
};
|
||||
|
|
|
@ -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 {};
|
||||
|
|
Loading…
Reference in New Issue