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-shallow-equal';
|
||||||
export * from './use-store-cart';
|
export * from './use-store-cart';
|
||||||
export * from './use-store-cart-coupons';
|
export * from './use-store-cart-coupons';
|
||||||
|
export * from './use-store-cart-item';
|
||||||
export * from './use-store-products';
|
export * from './use-store-products';
|
||||||
export * from './use-collection';
|
export * from './use-collection';
|
||||||
export * from './use-collection-header';
|
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 QuantitySelector from '@woocommerce/base-components/quantity-selector';
|
||||||
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
|
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
|
||||||
import { getCurrency, formatPrice } from '@woocommerce/base-utils';
|
import { getCurrency, formatPrice } from '@woocommerce/base-utils';
|
||||||
|
import { useStoreCartItem } from '@woocommerce/base-hooks';
|
||||||
import { Icon, trash } from '@woocommerce/icons';
|
import { Icon, trash } from '@woocommerce/icons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,6 +22,7 @@ import ProductLowStockBadge from './product-low-stock-badge';
|
||||||
*/
|
*/
|
||||||
const CartLineItemRow = ( { lineItem = {} } ) => {
|
const CartLineItemRow = ( { lineItem = {} } ) => {
|
||||||
const {
|
const {
|
||||||
|
key = '',
|
||||||
name = '',
|
name = '',
|
||||||
summary = '',
|
summary = '',
|
||||||
permalink = '',
|
permalink = '',
|
||||||
|
@ -36,6 +38,10 @@ const CartLineItemRow = ( { lineItem = {} } ) => {
|
||||||
const purchasePrice = parseInt( prices.price, 10 ) * lineQuantity;
|
const purchasePrice = parseInt( prices.price, 10 ) * lineQuantity;
|
||||||
const saleAmount = regularPrice - purchasePrice;
|
const saleAmount = regularPrice - purchasePrice;
|
||||||
|
|
||||||
|
const { removeItem, isPending: itemQuantityDisabled } = useStoreCartItem(
|
||||||
|
key
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className="wc-block-cart-items__row">
|
<tr className="wc-block-cart-items__row">
|
||||||
<td className="wc-block-cart-item__image">
|
<td className="wc-block-cart-item__image">
|
||||||
|
@ -60,14 +66,23 @@ const CartLineItemRow = ( { lineItem = {} } ) => {
|
||||||
</td>
|
</td>
|
||||||
<td className="wc-block-cart-item__quantity">
|
<td className="wc-block-cart-item__quantity">
|
||||||
<QuantitySelector
|
<QuantitySelector
|
||||||
|
disabled={ itemQuantityDisabled }
|
||||||
quantity={ lineQuantity }
|
quantity={ lineQuantity }
|
||||||
onChange={ setLineQuantity }
|
onChange={ setLineQuantity }
|
||||||
itemName={ name }
|
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' ) }
|
{ __( 'Remove item', 'woo-gutenberg-products-block' ) }
|
||||||
</button>
|
</button>
|
||||||
<button className="wc-block-cart-item__remove-icon">
|
<button
|
||||||
|
disabled={ itemQuantityDisabled }
|
||||||
|
className="wc-block-cart-item__remove-icon"
|
||||||
|
onClick={ removeItem }
|
||||||
|
>
|
||||||
<Icon srcElement={ trash } />
|
<Icon srcElement={ trash } />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
@ -100,6 +115,7 @@ const CartLineItemRow = ( { lineItem = {} } ) => {
|
||||||
|
|
||||||
CartLineItemRow.propTypes = {
|
CartLineItemRow.propTypes = {
|
||||||
lineItem: PropTypes.shape( {
|
lineItem: PropTypes.shape( {
|
||||||
|
key: PropTypes.string.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
summary: PropTypes.string.isRequired,
|
summary: PropTypes.string.isRequired,
|
||||||
images: PropTypes.array.isRequired,
|
images: PropTypes.array.isRequired,
|
||||||
|
|
|
@ -4,4 +4,6 @@ export const ACTION_TYPES = {
|
||||||
REPLACE_ERRORS: 'REPLACE_ERRORS',
|
REPLACE_ERRORS: 'REPLACE_ERRORS',
|
||||||
APPLYING_COUPON: 'APPLYING_COUPON',
|
APPLYING_COUPON: 'APPLYING_COUPON',
|
||||||
REMOVING_COUPON: 'REMOVING_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( '' );
|
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';
|
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.
|
* Reducer for receiving items related to the cart.
|
||||||
*
|
*
|
||||||
|
@ -13,6 +29,7 @@ import { ACTION_TYPES as types } from './action-types';
|
||||||
*/
|
*/
|
||||||
const reducer = (
|
const reducer = (
|
||||||
state = {
|
state = {
|
||||||
|
cartItemsQuantityPending: [],
|
||||||
cartData: {
|
cartData: {
|
||||||
coupons: [],
|
coupons: [],
|
||||||
items: [],
|
items: [],
|
||||||
|
@ -64,6 +81,32 @@ const reducer = (
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
break;
|
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;
|
return state;
|
||||||
};
|
};
|
||||||
|
|
|
@ -106,3 +106,27 @@ export const isRemovingCoupon = ( state ) => {
|
||||||
export const getCouponBeingRemoved = ( state ) => {
|
export const getCouponBeingRemoved = ( state ) => {
|
||||||
return state.metaData.removingCoupon || '';
|
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.
|
* @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