/**
* External dependencies
*/
import classnames from 'classnames';
import { __, sprintf } from '@wordpress/i18n';
import { speak } from '@wordpress/a11y';
import QuantitySelector from '@woocommerce/base-components/quantity-selector';
import ProductPrice from '@woocommerce/base-components/product-price';
import ProductName from '@woocommerce/base-components/product-name';
import {
useStoreCartItemQuantity,
useStoreEvents,
useStoreCart,
} from '@woocommerce/base-context/hooks';
import {
ProductBackorderBadge,
ProductImage,
ProductLowStockBadge,
ProductMetadata,
ProductSaleBadge,
} from '@woocommerce/base-components/cart-checkout';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import {
__experimentalApplyCheckoutFilter,
mustContain,
} from '@woocommerce/blocks-checkout';
import Dinero from 'dinero.js';
import { forwardRef, useMemo } from '@wordpress/element';
import type { CartItem } from '@woocommerce/types';
import { objectHasProp, Currency } from '@woocommerce/types';
import { getSetting } from '@woocommerce/settings';
/**
* Convert a Dinero object with precision to store currency minor unit.
*
* @param {Dinero} priceObject Price object to convert.
* @param {Object} currency Currency data.
* @return {number} Amount with new minor unit precision.
*/
const getAmountFromRawPrice = (
priceObject: Dinero.Dinero,
currency: Currency
) => {
return priceObject.convertPrecision( currency.minorUnit ).getAmount();
};
const productPriceValidation = ( value: string ) =>
mustContain( value, '' );
interface CartLineItemRowProps {
lineItem: CartItem | Record< string, never >;
onRemove?: () => void;
tabIndex?: number;
}
/**
* Cart line item table row component.
*/
const CartLineItemRow: React.ForwardRefExoticComponent<
CartLineItemRowProps & React.RefAttributes< HTMLTableRowElement >
> = forwardRef< HTMLTableRowElement, CartLineItemRowProps >(
(
{ lineItem, onRemove = () => void null, tabIndex },
ref
): JSX.Element => {
const {
name: initialName = '',
catalog_visibility: catalogVisibility = 'visible',
short_description: shortDescription = '',
description: fullDescription = '',
low_stock_remaining: lowStockRemaining = null,
show_backorder_badge: showBackorderBadge = false,
quantity_limits: quantityLimits = {
minimum: 1,
maximum: 99,
multiple_of: 1,
editable: true,
},
sold_individually: soldIndividually = false,
permalink = '',
images = [],
variation = [],
item_data: itemData = [],
prices = {
currency_code: 'USD',
currency_minor_unit: 2,
currency_symbol: '$',
currency_prefix: '$',
currency_suffix: '',
currency_decimal_separator: '.',
currency_thousand_separator: ',',
price: '0',
regular_price: '0',
sale_price: '0',
price_range: null,
raw_prices: {
precision: 6,
price: '0',
regular_price: '0',
sale_price: '0',
},
},
totals = {
currency_code: 'USD',
currency_minor_unit: 2,
currency_symbol: '$',
currency_prefix: '$',
currency_suffix: '',
currency_decimal_separator: '.',
currency_thousand_separator: ',',
line_subtotal: '0',
line_subtotal_tax: '0',
},
extensions,
} = lineItem;
const { quantity, setItemQuantity, removeItem, isPendingDelete } =
useStoreCartItemQuantity( lineItem );
const { dispatchStoreEvent } = useStoreEvents();
// Prepare props to pass to the __experimentalApplyCheckoutFilter filter.
// We need to pluck out receiveCart.
// eslint-disable-next-line no-unused-vars
const { receiveCart, ...cart } = useStoreCart();
const arg = useMemo(
() => ( {
context: 'cart',
cartItem: lineItem,
cart,
} ),
[ lineItem, cart ]
);
const priceCurrency = getCurrencyFromPriceResponse( prices );
const name = __experimentalApplyCheckoutFilter( {
filterName: 'itemName',
defaultValue: initialName,
extensions,
arg,
} );
const regularAmountSingle = Dinero( {
amount: parseInt( prices.raw_prices.regular_price, 10 ),
precision: prices.raw_prices.precision,
} );
const purchaseAmountSingle = Dinero( {
amount: parseInt( prices.raw_prices.price, 10 ),
precision: prices.raw_prices.precision,
} );
const saleAmountSingle =
regularAmountSingle.subtract( purchaseAmountSingle );
const saleAmount = saleAmountSingle.multiply( quantity );
const totalsCurrency = getCurrencyFromPriceResponse( totals );
let lineSubtotal = parseInt( totals.line_subtotal, 10 );
if ( getSetting( 'displayCartPricesIncludingTax', false ) ) {
lineSubtotal += parseInt( totals.line_subtotal_tax, 10 );
}
const subtotalPrice = Dinero( {
amount: lineSubtotal,
precision: totalsCurrency.minorUnit,
} );
const firstImage = images.length ? images[ 0 ] : {};
const isProductHiddenFromCatalog =
catalogVisibility === 'hidden' || catalogVisibility === 'search';
const cartItemClassNameFilter = __experimentalApplyCheckoutFilter( {
filterName: 'cartItemClass',
defaultValue: '',
extensions,
arg,
} );
// Allow extensions to filter how the price is displayed. Ie: prepending or appending some values.
const productPriceFormat = __experimentalApplyCheckoutFilter( {
filterName: 'cartItemPrice',
defaultValue: '',
extensions,
arg,
validation: productPriceValidation,
} );
const subtotalPriceFormat = __experimentalApplyCheckoutFilter( {
filterName: 'subtotalPriceFormat',
defaultValue: '',
extensions,
arg,
validation: productPriceValidation,
} );
const saleBadgePriceFormat = __experimentalApplyCheckoutFilter( {
filterName: 'saleBadgePriceFormat',
defaultValue: '',
extensions,
arg,
validation: productPriceValidation,
} );
const showRemoveItemLink = __experimentalApplyCheckoutFilter( {
filterName: 'showRemoveItemLink',
defaultValue: true,
extensions,
arg,
} );
return (
{ /* If the image has no alt text, this link is unnecessary and can be hidden. */ }
{ /* We don't need to make it focusable, because product name has the same link. */ }
{ isProductHiddenFromCatalog ? (
) : (
) }
|
{ showBackorderBadge ? (
) : (
!! lowStockRemaining && (
)
) }
{ ! soldIndividually &&
!! quantityLimits.editable && (
{
setItemQuantity( newQuantity );
dispatchStoreEvent(
'cart-set-item-quantity',
{
product: lineItem,
quantity: newQuantity,
}
);
} }
itemName={ name }
/>
) }
{ showRemoveItemLink && (
) }
|
|
);
}
);
export default CartLineItemRow;