Refactor cart shipping settings and shipping calculator (https://github.com/woocommerce/woocommerce-blocks/pull/1943)

* Move cart attributes to attributes file

* Stop feedback prompt jumping around; consolodate strings

* Update option labels and descriptions

* Match checkout save function

* hasShippingRate helper

* Refactor full cart/frontend views for shipping calc

* Add hasShippingAddress to useShippingRates hook

* Initial shipping calculator in totals row implementation

* Create cart context

* Update preview data to match API response

* Use context provider for cart

* Provide default cart item for placeholder with correct shape

* Remove outdated shape validation from cartlineitemrow

* Use preview data in editor context

* Tidy up components

* Tests/lint

* Update assets/js/base/components/totals/totals-shipping-item/has-shipping-rate.js

Co-Authored-By: Seghir Nadir <nadir.seghir@gmail.com>

* No need to camel case previewdata

* Use isValidElement

* Implement EditorContext

* Use select if no post is given

Co-authored-by: Seghir Nadir <nadir.seghir@gmail.com>
This commit is contained in:
Mike Jolley 2020-03-13 13:41:59 +00:00 committed by GitHub
parent c20f1bd7bf
commit 128ac6d63d
25 changed files with 513 additions and 333 deletions

View File

@ -13,13 +13,17 @@ import isShallowEqual from '@wordpress/is-shallow-equal';
import './style.scss';
import AddressForm from '../address-form';
const ShippingCalculatorAddress = ( { address: initialAddress, onUpdate } ) => {
const ShippingCalculatorAddress = ( {
address: initialAddress,
onUpdate,
addressFields,
} ) => {
const [ address, setAddress ] = useState( initialAddress );
return (
<form className="wc-block-shipping-calculator-address">
<AddressForm
fields={ [ 'country', 'state', 'city', 'postcode' ] }
fields={ addressFields }
onChange={ setAddress }
values={ address }
/>
@ -39,13 +43,9 @@ const ShippingCalculatorAddress = ( { address: initialAddress, onUpdate } ) => {
};
ShippingCalculatorAddress.propTypes = {
address: PropTypes.shape( {
city: PropTypes.string,
state: PropTypes.string,
postcode: PropTypes.string,
country: PropTypes.string,
} ),
address: PropTypes.object.isRequired,
onUpdate: PropTypes.func.isRequired,
addressFields: PropTypes.array.isRequired,
};
export default ShippingCalculatorAddress;

View File

@ -2,8 +2,6 @@
* External dependencies
*/
import PropTypes from 'prop-types';
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
/**
* Internal dependencies
@ -11,44 +9,24 @@ import { useState } from '@wordpress/element';
import ShippingCalculatorAddress from './address';
import './style.scss';
const ShippingCalculator = ( { address, setAddress } ) => {
const [ isShippingCalculatorOpen, setIsShippingCalculatorOpen ] = useState(
false
);
const ShippingCalculator = ( { onUpdate, address, addressFields } ) => {
return (
<span className="wc-block-cart__change-address">
(
<button
className="wc-block-cart__change-address-button"
onClick={ () => {
setIsShippingCalculatorOpen( ! isShippingCalculatorOpen );
<div className="wc-block-cart__shipping-calculator">
<ShippingCalculatorAddress
address={ address }
addressFields={ addressFields }
onUpdate={ ( newAddress ) => {
onUpdate( newAddress );
} }
>
{ __( 'change address', 'woo-gutenberg-products-block' ) }
</button>
)
{ isShippingCalculatorOpen && (
<ShippingCalculatorAddress
address={ address }
onUpdate={ ( newAddress ) => {
setAddress( newAddress );
setIsShippingCalculatorOpen( false );
} }
/>
) }
</span>
/>
</div>
);
};
ShippingCalculator.propTypes = {
address: PropTypes.shape( {
city: PropTypes.string,
state: PropTypes.string,
postcode: PropTypes.string,
country: PropTypes.string,
} ),
setAddress: PropTypes.func.isRequired,
onUpdate: PropTypes.func.isRequired,
address: PropTypes.object.isRequired,
addressFields: PropTypes.array.isRequired,
};
export default ShippingCalculator;

View File

@ -26,8 +26,14 @@
white-space: pre;
}
.wc-block-cart__shipping-address,
.wc-block-cart__shipping-address button {
color: $core-grey-dark-400;
}
.wc-block-shipping-rates-control__no-results {
margin-bottom: 0;
font-style: italic;
}
// Resets when it's inside a panel.

View File

@ -3,6 +3,7 @@
*/
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { isValidElement } from '@wordpress/element';
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
/**
@ -16,23 +17,29 @@ const TotalsItem = ( { className, currency, label, value, description } ) => {
className={ classnames( 'wc-block-totals-table-item', className ) }
>
<span className="wc-block-totals-table-item__label">{ label }</span>
<FormattedMonetaryAmount
className="wc-block-totals-table-item__value"
currency={ currency }
displayType="text"
value={ value }
/>
<span className="wc-block-totals-table-item__description">
{ isValidElement( value ) ? (
<div className="wc-block-totals-table-item__value">
{ value }
</div>
) : (
<FormattedMonetaryAmount
className="wc-block-totals-table-item__value"
currency={ currency }
displayType="text"
value={ value }
/>
) }
<div className="wc-block-totals-table-item__description">
{ description }
</span>
</div>
</div>
);
};
TotalsItem.propTypes = {
currency: PropTypes.object.isRequired,
currency: PropTypes.object,
label: PropTypes.string.isRequired,
value: PropTypes.number.isRequired,
value: PropTypes.oneOfType( [ PropTypes.number, PropTypes.node ] ),
className: PropTypes.string,
description: PropTypes.node,
};

View File

@ -0,0 +1,12 @@
/**
* Searches an array of packages/rates to see if there are actually any rates
* available.
*
* @param {Array} shippingRatePackages An array of packages and rates.
* @return {boolean} True if a rate exists.
*/
const hasShippingRate = ( shippingRatePackages ) => {
return shippingRatePackages.some( shippingRatePackage => shippingRatePackage.shipping_rates.length );
};
export default hasShippingRate;

View File

@ -2,69 +2,157 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import {
DISPLAY_CART_PRICES_INCLUDING_TAX,
SHIPPING_ENABLED,
} from '@woocommerce/block-settings';
import { DISPLAY_CART_PRICES_INCLUDING_TAX } from '@woocommerce/block-settings';
import ShippingCalculator from '@woocommerce/base-components/shipping-calculator';
import ShippingLocation from '@woocommerce/base-components/shipping-location';
import PropTypes from 'prop-types';
import { useState } from '@wordpress/element';
import { useShippingRates } from '@woocommerce/base-hooks';
/**
* Internal dependencies
*/
import TotalsItem from '../totals-item';
import ShippingRateSelector from './shipping-rate-selector';
import hasShippingRate from './has-shipping-rate';
/**
* Renders the shipping totals row, rates, and calculator if enabled.
*/
const TotalsShippingItem = ( {
currency,
shippingAddress,
updateShippingAddress,
values,
showCalculator = true,
showRatesWithoutAddress = false,
} ) => {
if ( ! SHIPPING_ENABLED ) {
return null;
}
const [ isShippingCalculatorOpen, setIsShippingCalculatorOpen ] = useState(
false
);
const defaultAddressFields = [ 'country', 'state', 'city', 'postcode' ];
const {
total_shipping: totalShipping,
total_shipping_tax: totalShippingTax,
} = values;
const shippingValue = parseInt( totalShipping, 10 );
const shippingTaxValue = parseInt( totalShippingTax, 10 );
shippingRates,
shippingAddress,
shippingRatesLoading,
hasShippingAddress,
setShippingAddress,
} = useShippingRates( defaultAddressFields );
const totalShippingValue = DISPLAY_CART_PRICES_INCLUDING_TAX
? parseInt( values.total_shipping, 10 ) +
parseInt( values.total_shipping_tax, 10 )
: parseInt( values.total_shipping, 10 );
const hasRates = hasShippingRate( shippingRates ) || totalShippingValue;
const showingRates = showRatesWithoutAddress || hasShippingAddress;
// If we have no rates, and an address is needed.
if ( ! hasRates && ! hasShippingAddress ) {
return (
<TotalsItem
label={ __( 'Shipping', 'woo-gutenberg-products-block' ) }
value={
showCalculator ? (
<button
className="wc-block-cart__change-address-button"
onClick={ () => {
setIsShippingCalculatorOpen(
! isShippingCalculatorOpen
);
} }
>
{ __(
'Calculate',
'woo-gutenberg-products-block'
) }
</button>
) : (
<em>
{ __(
'Calculated during checkout',
'woo-gutenberg-products-block'
) }
</em>
)
}
description={
<>
{ showCalculator && isShippingCalculatorOpen && (
<ShippingCalculator
onUpdate={ ( newAddress ) => {
setShippingAddress( newAddress );
setIsShippingCalculatorOpen( false );
} }
address={ shippingAddress }
addressFields={ defaultAddressFields }
/>
) }
</>
}
/>
);
}
return (
<TotalsItem
currency={ currency }
description={
<>
{ shippingAddress && (
<ShippingLocation address={ shippingAddress } />
) }
{ updateShippingAddress && shippingAddress && (
<ShippingCalculator
address={ shippingAddress }
setAddress={ updateShippingAddress }
/>
) }
</>
}
label={ __( 'Shipping', 'woo-gutenberg-products-block' ) }
value={
DISPLAY_CART_PRICES_INCLUDING_TAX
? shippingValue + shippingTaxValue
: shippingValue
}
/>
<>
<TotalsItem
label={ __( 'Shipping', 'woo-gutenberg-products-block' ) }
value={ totalShippingValue ? totalShippingValue : '' }
description={
<>
<ShippingLocation address={ shippingAddress } />{ ' ' }
{ showCalculator && (
<button
className="wc-block-cart__change-address-button"
onClick={ () => {
setIsShippingCalculatorOpen(
! isShippingCalculatorOpen
);
} }
>
{ __(
'(change address)',
'woo-gutenberg-products-block'
) }
</button>
) }
{ showCalculator && isShippingCalculatorOpen && (
<ShippingCalculator
onUpdate={ ( newAddress ) => {
setShippingAddress( newAddress );
setIsShippingCalculatorOpen( false );
} }
address={ shippingAddress }
addressFields={ defaultAddressFields }
/>
) }
</>
}
currency={ currency }
/>
{ showingRates && (
<fieldset className="wc-block-cart__shipping-options-fieldset">
<legend className="screen-reader-text">
{ __(
'Choose a shipping method',
'woo-gutenberg-products-block'
) }
</legend>
<ShippingRateSelector
shippingRates={ shippingRates }
shippingRatesLoading={ shippingRatesLoading }
/>
</fieldset>
) }
</>
);
};
TotalsShippingItem.propTypes = {
currency: PropTypes.object.isRequired,
shippingAddress: PropTypes.object,
updateShippingAddress: PropTypes.func,
values: PropTypes.shape( {
total_shipping: PropTypes.string,
total_shipping_tax: PropTypes.string,
} ).isRequired,
showCalculator: PropTypes.bool,
showRatesWithoutAddress: PropTypes.bool,
};
export default TotalsShippingItem;

View File

@ -0,0 +1,51 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
import { decodeEntities } from '@wordpress/html-entities';
import { getCurrencyFromPriceResponse } from '@woocommerce/base-utils';
import ShippingRatesControl from '@woocommerce/base-components/shipping-rates-control';
const renderShippingRatesControlOption = ( option ) => ( {
label: decodeEntities( option.name ),
value: option.rate_id,
description: (
<>
{ option.price && (
<FormattedMonetaryAmount
currency={ getCurrencyFromPriceResponse( option ) }
value={ option.price }
/>
) }
{ option.price && option.delivery_time ? ' — ' : null }
{ decodeEntities( option.delivery_time ) }
</>
),
} );
const ShippingRateSelector = ( { shippingRates, shippingRatesLoading } ) => {
return (
<fieldset className="wc-block-cart__shipping-options-fieldset">
<legend className="screen-reader-text">
{ __(
'Choose the shipping method.',
'woo-gutenberg-products-block'
) }
</legend>
<ShippingRatesControl
className="wc-block-cart__shipping-options"
collapsibleWhenMultiple={ true }
noResultsMessage={ __(
'No shipping options were found.',
'woo-gutenberg-products-block'
) }
renderOption={ renderShippingRatesControlOption }
shippingRates={ shippingRates }
shippingRatesLoading={ shippingRatesLoading }
/>
</fieldset>
);
};
export default ShippingRateSelector;

View File

@ -14,7 +14,7 @@ const SET_BILLING_DATA = 'set_billing_data';
/**
* Used to dispatch a status update only for the given type.
*
* @param type
* @param {string} type
*
* @return {Object} The action object.
*/

View File

@ -0,0 +1,55 @@
/**
* External dependencies
*/
import { createContext, useContext } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
/**
* @typedef {import('@woocommerce/type-defs/contexts').EditorDataContext} EditorDataContext
*/
const EditorContext = createContext( {
isEditor: false,
currentPostId: 0,
} );
/**
* @return {EditorDataContext} Returns the editor data context value
*/
export const useEditorContext = () => {
return useContext( EditorContext );
};
/**
* Editor provider
*
* @param {Object} props Incoming props for the provider.
* @param {*} props.children The children being wrapped.
* @param {number} props.currentPostId The post being edited.
*/
export const EditorProvider = ( { children, currentPostId = 0 } ) => {
const editingPostId = useSelect(
( select ) => {
if ( ! currentPostId ) {
const store = select( 'core/editor' );
return store.getCurrentPostId();
}
return currentPostId;
},
[ currentPostId ]
);
/**
* @type {EditorDataContext}
*/
const editorData = {
isEditor: true,
currentPostId: editingPostId,
};
return (
<EditorContext.Provider value={ editorData }>
{ children }
</EditorContext.Provider>
);
};

View File

@ -3,3 +3,4 @@ export * from './inner-block-configuration-context';
export * from './product-layout-context';
export * from './query-state-context';
export * from './store-notices-context';
export * from './editor';

View File

@ -5,6 +5,8 @@
*/
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { useSelect } from '@wordpress/data';
import { useEditorContext } from '@woocommerce/base-context';
import { previewCart } from '@woocommerce/resource-previews';
/**
* @constant
@ -12,6 +14,7 @@ import { useSelect } from '@wordpress/data';
*/
const defaultCartData = {
cartCoupons: [],
shippingRates: [],
cartItems: [],
cartItemsCount: 0,
cartItemsWeight: 0,
@ -19,7 +22,6 @@ const defaultCartData = {
cartTotals: {},
cartIsLoading: true,
cartErrors: [],
shippingRates: [],
};
/**
@ -35,6 +37,7 @@ const defaultCartData = {
* @return {StoreCart} Object containing cart data.
*/
export const useStoreCart = ( options = { shouldSelect: true } ) => {
const { isEditor } = useEditorContext();
const { shouldSelect } = options;
const results = useSelect(
@ -42,6 +45,21 @@ export const useStoreCart = ( options = { shouldSelect: true } ) => {
if ( ! shouldSelect ) {
return null;
}
if ( isEditor ) {
return {
cartCoupons: previewCart.coupons,
shippingRates: previewCart.shipping_rates,
cartItems: previewCart.items,
cartItemsCount: previewCart.items_count,
cartItemsWeight: previewCart.items_weight,
cartNeedsShipping: previewCart.needs_shipping,
cartTotals: previewCart.totals,
cartIsLoading: false,
cartErrors: [],
};
}
const store = select( storeKey );
const cartData = store.getCartData();
const cartErrors = store.getCartErrors();
@ -49,7 +67,6 @@ export const useStoreCart = ( options = { shouldSelect: true } ) => {
const cartIsLoading = ! store.hasFinishedResolution(
'getCartData'
);
return {
cartCoupons: cartData.coupons,
shippingRates: cartData.shippingRates,

View File

@ -25,6 +25,7 @@ import { pluckAddress } from '../../utils';
* - {Function} setShippingAddress An function that optimistically
* update shipping address and dispatches async rate fetching.
* - {Object} shippingAddress An object containing shipping address.
* - {Object} shippingAddress True when address data exists.
*/
export const useShippingRates = ( addressFieldsKeys ) => {
const { cartErrors, shippingRates } = useStoreCart();
@ -59,11 +60,13 @@ export const useShippingRates = ( addressFieldsKeys ) => {
updateShippingAddress( debouncedShippingAddress );
}
}, [ debouncedShippingAddress ] );
return {
shippingRates,
shippingAddress,
setShippingAddress,
shippingRatesLoading,
shippingRatesErrors: cartErrors,
hasShippingAddress: !! shippingAddress.country,
};
};

View File

@ -0,0 +1,20 @@
/**
* External dependencies
*/
import {
IS_SHIPPING_CALCULATOR_ENABLED,
IS_SHIPPING_COST_HIDDEN,
} from '@woocommerce/block-settings';
const blockAttributes = {
isShippingCalculatorEnabled: {
type: 'boolean',
default: IS_SHIPPING_CALCULATOR_ENABLED,
},
isShippingCostHidden: {
type: 'boolean',
default: IS_SHIPPING_COST_HIDDEN,
},
};
export default blockAttributes;

View File

@ -8,12 +8,9 @@ import { Disabled, PanelBody, ToggleControl } from '@wordpress/components';
import PropTypes from 'prop-types';
import { withFeedbackPrompt } from '@woocommerce/block-hocs';
import ViewSwitcher from '@woocommerce/block-components/view-switcher';
import {
previewCart,
previewShippingRates,
} from '@woocommerce/resource-previews';
import { SHIPPING_ENABLED } from '@woocommerce/block-settings';
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
import { EditorProvider } from '@woocommerce/base-context';
/**
* Internal dependencies
@ -30,10 +27,7 @@ const CartEditor = ( { className, attributes, setAttributes } ) => {
const BlockSettings = () => (
<InspectorControls>
<PanelBody
title={ __(
'Shipping calculations',
'woo-gutenberg-products-block'
) }
title={ __( 'Shipping rates', 'woo-gutenberg-products-block' ) }
>
<ToggleControl
label={ __(
@ -41,7 +35,7 @@ const CartEditor = ( { className, attributes, setAttributes } ) => {
'woo-gutenberg-products-block'
) }
help={ __(
'Allow customers to estimate shipping.',
'Allow customers to estimate shipping by entering their address.',
'woo-gutenberg-products-block'
) }
checked={ isShippingCalculatorEnabled }
@ -51,24 +45,22 @@ const CartEditor = ( { className, attributes, setAttributes } ) => {
} )
}
/>
{ isShippingCalculatorEnabled && (
<ToggleControl
label={ __(
'Hide shipping costs',
'woo-gutenberg-products-block'
) }
help={ __(
'Hide shipping costs until an address is entered.',
'woo-gutenberg-products-block'
) }
checked={ isShippingCostHidden }
onChange={ () =>
setAttributes( {
isShippingCostHidden: ! isShippingCostHidden,
} )
}
/>
) }
<ToggleControl
label={ __(
'Hide shipping costs until an address is entered',
'woo-gutenberg-products-block'
) }
help={ __(
'If checked, shipping rates will be hidden until the customer uses the shipping calculator or enters their address during checkout.',
'woo-gutenberg-products-block'
) }
checked={ isShippingCostHidden }
onChange={ () =>
setAttributes( {
isShippingCostHidden: ! isShippingCostHidden,
} )
}
/>
</PanelBody>
</InspectorControls>
);
@ -112,19 +104,16 @@ const CartEditor = ( { className, attributes, setAttributes } ) => {
) }
>
<Disabled>
<FullCart
cartItems={ previewCart.items }
cartTotals={ previewCart.totals }
isShippingCostHidden={
isShippingCostHidden
}
isShippingCalculatorEnabled={
isShippingCalculatorEnabled
}
shippingRates={
previewShippingRates
}
/>
<EditorProvider>
<FullCart
isShippingCostHidden={
isShippingCostHidden
}
isShippingCalculatorEnabled={
isShippingCalculatorEnabled
}
/>
</EditorProvider>
</Disabled>
</BlockErrorBoundary>
</>
@ -159,7 +148,7 @@ CartEditor.propTypes = {
export default withFeedbackPrompt(
__(
'We are currently working on improving our cart and providing merchants with tools and options to customize their cart to their stores needs.',
'We are currently working on improving our cart and checkout blocks, providing merchants with the tools and customization options they need.',
'woo-gutenberg-products-block'
)
)( CartEditor );

View File

@ -17,53 +17,69 @@ import { __experimentalCreateInterpolateElement } from 'wordpress-element';
* Internal dependencies
*/
import FullCart from './full-cart';
import blockAttributes from './attributes';
import renderFrontend from '../../../utils/render-frontend.js';
/**
* Wrapper component to supply API data and show empty cart view as needed.
* Renders the frontend block within the cart provider.
*/
const CartFrontend = ( {
emptyCart,
isShippingCalculatorEnabled,
isShippingCostHidden,
} ) => {
const {
cartItems,
cartTotals,
cartIsLoading,
cartCoupons,
shippingRates,
} = useStoreCart();
const Block = ( { emptyCart, attributes } ) => {
const { cartItems, cartIsLoading } = useStoreCart();
return (
<StoreNoticesProvider context="wc/cart">
{ ! cartIsLoading && ! cartItems.length ? (
<>
{ ! cartIsLoading && cartItems.length === 0 ? (
<RawHTML>{ emptyCart }</RawHTML>
) : (
<LoadingMask showSpinner={ true } isLoading={ cartIsLoading }>
<FullCart
cartItems={ cartItems }
cartTotals={ cartTotals }
cartCoupons={ cartCoupons }
isShippingCalculatorEnabled={
isShippingCalculatorEnabled
attributes.isShippingCalculatorEnabled
}
isShippingCostHidden={ isShippingCostHidden }
isLoading={ cartIsLoading }
shippingRates={ shippingRates }
isShippingCostHidden={ attributes.isShippingCostHidden }
/>
</LoadingMask>
) }
</>
);
};
/**
* Wrapper component to supply API data and show empty cart view as needed.
*
* @param {*} props
*/
const CartFrontend = ( props ) => {
return (
<StoreNoticesProvider context="wc/cart">
<Block { ...props } />
</StoreNoticesProvider>
);
};
const getProps = ( el ) => ( {
emptyCart: el.innerHTML,
isShippingCalculatorEnabled:
el.dataset.isShippingCalculatorEnabled === 'true',
isShippingCostHidden: el.dataset.isShippingCostHidden === 'true',
} );
const getProps = ( el ) => {
const attributes = {};
Object.keys( blockAttributes ).forEach( ( key ) => {
if ( typeof el.dataset[ key ] !== 'undefined' ) {
if (
el.dataset[ key ] === 'true' ||
el.dataset[ key ] === 'false'
) {
attributes[ key ] = el.dataset[ key ] !== 'false';
} else {
attributes[ key ] = el.dataset[ key ];
}
} else {
attributes[ key ] = blockAttributes[ key ].default;
}
} );
return {
emptyCart: el.innerHTML,
attributes,
};
};
const getErrorBoundaryProps = () => {
return {

View File

@ -42,9 +42,6 @@ const getMaximumQuantity = ( backOrdersAllowed, lowStockAmount ) => {
* Cart line item table row component.
*/
const CartLineItemRow = ( { lineItem } ) => {
/**
* @type {CartItem}
*/
const {
name,
summary,
@ -142,32 +139,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,
low_stock_remaining: PropTypes.oneOfType( [
PropTypes.number,
PropTypes.oneOf( [ null ] ),
] ),
backorders_allowed: PropTypes.bool.isRequired,
sold_individually: PropTypes.bool.isRequired,
variation: PropTypes.arrayOf(
PropTypes.shape( {
attribute: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
} )
).isRequired,
totals: PropTypes.shape( {
line_subtotal: PropTypes.string.isRequired,
line_total: PropTypes.string.isRequired,
} ).isRequired,
prices: PropTypes.shape( {
price: PropTypes.string.isRequired,
regular_price: PropTypes.string.isRequired,
} ).isRequired,
} ),
lineItem: PropTypes.object.isRequired,
};
export default CartLineItemRow;

View File

@ -10,7 +10,51 @@ import PropTypes from 'prop-types';
import CartLineItemRow from './cart-line-item-row';
const placeholderRows = [ ...Array( 3 ) ].map( ( _x, i ) => (
<CartLineItemRow key={ i } />
<CartLineItemRow
key={ i }
lineItem={ {
key: '1',
id: 1,
quantity: 2,
name: '',
summary: '',
short_description: '',
description: '',
sku: '',
low_stock_remaining: null,
backorders_allowed: false,
sold_individually: false,
permalink: '',
images: [],
variation: [],
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,
},
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',
line_total: '0',
line_total_tax: '0',
},
} }
/>
) );
const CartLineItemsTable = ( { lineItems = [], isLoading = false } ) => {

View File

@ -13,17 +13,14 @@ import {
TotalsShippingItem,
TotalsTaxesItem,
} from '@woocommerce/base-components/totals';
import ShippingRatesControl from '@woocommerce/base-components/shipping-rates-control';
import {
COUPONS_ENABLED,
SHIPPING_ENABLED,
DISPLAY_CART_PRICES_INCLUDING_TAX,
SHIPPING_ENABLED,
} from '@woocommerce/block-settings';
import { getCurrencyFromPriceResponse } from '@woocommerce/base-utils';
import { Card, CardBody } from 'wordpress-components';
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
import { decodeEntities } from '@wordpress/html-entities';
import { useStoreCartCoupons, useShippingRates } from '@woocommerce/base-hooks';
import { useStoreCartCoupons, useStoreCart } from '@woocommerce/base-hooks';
import classnames from 'classnames';
import {
Sidebar,
@ -41,85 +38,30 @@ import CartLineItemsTable from './cart-line-items-table';
import './style.scss';
import './editor.scss';
const renderShippingRatesControlOption = ( option ) => ( {
label: decodeEntities( option.name ),
value: option.rate_id,
description: (
<>
{ option.price && (
<FormattedMonetaryAmount
currency={ getCurrencyFromPriceResponse( option ) }
value={ option.price }
/>
) }
{ option.price && option.delivery_time ? ' — ' : null }
{ decodeEntities( option.delivery_time ) }
</>
),
} );
const ShippingCalculatorOptions = ( {
shippingRates,
shippingRatesLoading,
} ) => {
return (
<fieldset className="wc-block-cart__shipping-options-fieldset">
<legend className="screen-reader-text">
{ __(
'Choose the shipping method.',
'woo-gutenberg-products-block'
) }
</legend>
<ShippingRatesControl
className="wc-block-cart__shipping-options"
collapsibleWhenMultiple={ true }
noResultsMessage={ __(
'No shipping options were found.',
'woo-gutenberg-products-block'
) }
renderOption={ renderShippingRatesControlOption }
shippingRates={ shippingRates }
shippingRatesLoading={ shippingRatesLoading }
/>
</fieldset>
);
};
/**
* Component that renders the Cart block when user has something in cart aka "full".
*/
const Cart = ( {
cartItems = [],
cartTotals = {},
cartCoupons = [],
isShippingCalculatorEnabled,
isShippingCostHidden,
shippingRates,
isLoading = false,
} ) => {
const defaultAddressFields = [ 'country', 'state', 'city', 'postcode' ];
const {
shippingAddress,
setShippingAddress,
shippingRatesLoading,
} = useShippingRates( defaultAddressFields );
const Cart = ( { isShippingCalculatorEnabled, isShippingCostHidden } ) => {
const { cartItems, cartTotals, cartIsLoading, cartErrors } = useStoreCart();
const {
applyCoupon,
removeCoupon,
isApplyingCoupon,
isRemovingCoupon,
cartCoupons,
cartCouponsErrors,
} = useStoreCartCoupons();
const showShippingCosts = Boolean(
SHIPPING_ENABLED &&
isShippingCalculatorEnabled &&
( ! isShippingCostHidden || shippingAddress?.country )
);
const errors = [ ...cartErrors, ...cartCouponsErrors ];
if ( errors.length > 0 ) {
throw new Error( errors[ 0 ].message );
}
const totalsCurrency = getCurrencyFromPriceResponse( cartTotals );
const cartClassName = classnames( 'wc-block-cart', {
'wc-block-cart--is-loading': isLoading,
'wc-block-cart--is-loading': cartIsLoading,
} );
return (
@ -128,7 +70,7 @@ const Cart = ( {
<CartLineItemsTitle itemCount={ cartItems.length } />
<CartLineItemsTable
lineItems={ cartItems }
isLoading={ isLoading }
isLoading={ cartIsLoading }
/>
</Main>
<Sidebar className="wc-block-cart__sidebar">
@ -155,30 +97,16 @@ const Cart = ( {
removeCoupon={ removeCoupon }
values={ cartTotals }
/>
{ isShippingCalculatorEnabled && (
{ SHIPPING_ENABLED && (
<TotalsShippingItem
currency={ totalsCurrency }
shippingAddress={ shippingAddress }
updateShippingAddress={ setShippingAddress }
showCalculator={ isShippingCalculatorEnabled }
showRatesWithoutAddress={
! isShippingCostHidden
}
values={ cartTotals }
currency={ totalsCurrency }
/>
) }
{ showShippingCosts && (
<fieldset className="wc-block-cart__shipping-options-fieldset">
<legend className="screen-reader-text">
{ __(
'Choose the shipping method.',
'woo-gutenberg-products-block'
) }
</legend>
<ShippingCalculatorOptions
shippingRates={ shippingRates }
shippingRatesLoading={
shippingRatesLoading
}
/>
</fieldset>
) }
{ ! DISPLAY_CART_PRICES_INCLUDING_TAX && (
<TotalsTaxesItem
currency={ totalsCurrency }
@ -188,7 +116,6 @@ const Cart = ( {
{ COUPONS_ENABLED && (
<TotalsCouponCodeInput
onSubmit={ applyCoupon }
initialOpen={ true }
isLoading={ isApplyingCoupon }
/>
) }
@ -205,26 +132,8 @@ const Cart = ( {
};
Cart.propTypes = {
cartItems: PropTypes.array,
cartTotals: PropTypes.shape( {
total_items: PropTypes.string,
total_items_tax: PropTypes.string,
total_fees: PropTypes.string,
total_fees_tax: PropTypes.string,
total_discount: PropTypes.string,
total_discount_tax: PropTypes.string,
total_shipping: PropTypes.string,
total_shipping_tax: PropTypes.string,
total_tax: PropTypes.string,
total_price: PropTypes.string,
} ),
isShippingCalculatorEnabled: PropTypes.bool,
isShippingCostHidden: PropTypes.bool,
isLoading: PropTypes.bool,
/**
* List of shipping rates to display. If defined, shipping rates will not be fetched from the API (used for the block preview).
*/
shippingRates: PropTypes.array,
};
export default Cart;

View File

@ -4,7 +4,7 @@
.wc-block-cart__item-count {
float: right;
}
.wc-block-cart__change-address {
.wc-block-cart__shipping-calculator {
white-space: nowrap;
}
.wc-block-cart__change-address-button {

View File

@ -2,14 +2,11 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';
import { InnerBlocks } from '@wordpress/block-editor';
import { registerBlockType } from '@wordpress/blocks';
import { Icon, cart } from '@woocommerce/icons';
import {
IS_SHIPPING_CALCULATOR_ENABLED,
IS_SHIPPING_COST_HIDDEN,
} from '@woocommerce/block-settings';
import { kebabCase } from 'lodash';
import classnames from 'classnames';
/**
* Internal dependencies
@ -17,6 +14,7 @@ import {
import edit from './edit';
import { example } from './example';
import './style.scss';
import blockAttributes from './attributes';
/**
* Register and run the Cart block.
@ -36,35 +34,27 @@ const settings = {
multiple: false,
},
example,
attributes: {
isShippingCalculatorEnabled: {
type: 'boolean',
default: IS_SHIPPING_CALCULATOR_ENABLED,
},
isShippingCostHidden: {
type: 'boolean',
default: IS_SHIPPING_COST_HIDDEN,
},
},
attributes: blockAttributes,
edit,
/**
* Save the props to post content.
*/
save( { attributes } ) {
const {
className,
isShippingCalculatorEnabled,
isShippingCostHidden,
} = attributes;
const data = {
'data-is-shipping-calculator-enabled': isShippingCalculatorEnabled,
'data-is-shipping-cost-hidden': isShippingCostHidden,
};
const data = {};
Object.keys( blockAttributes ).forEach( ( key ) => {
if (
blockAttributes[ key ].save !== false &&
typeof attributes[ key ] !== 'undefined'
) {
data[ 'data-' + kebabCase( key ) ] = attributes[ key ];
}
} );
return (
<div
className={ classNames( 'is-loading', className ) }
className={ classnames( 'is-loading', attributes.className ) }
{ ...data }
>
<InnerBlocks.Content />

View File

@ -232,7 +232,7 @@ const CheckoutEditor = ( { attributes, setAttributes } ) => {
export default withFeedbackPrompt(
__(
'We are currently working on improving our checkout and providing merchants with tools and options to customize their checkout to their stores needs.',
'We are currently working on improving our cart and checkout blocks, providing merchants with the tools and customization options they need.',
'woo-gutenberg-products-block'
)
)( CheckoutEditor );

View File

@ -28,10 +28,10 @@ const withFeedbackPrompt = ( content ) =>
createHigherOrderComponent( ( BlockEdit ) => {
return ( props ) => (
<Fragment>
<BlockEdit { ...props } />
<InspectorControls>
<FeedbackPrompt text={ content } />
</InspectorControls>
<BlockEdit { ...props } />
</Fragment>
);
}, 'withFeedbackPrompt' );

View File

@ -7,11 +7,14 @@ import { __ } from '@wordpress/i18n';
* Internal dependencies
*/
import productPicture from './product-image';
import { previewShippingRates } from './shipping-rates';
// Sample data for cart block.
// This closely resembles the data returned from the Store API /cart endpoint.
// https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/master/src/RestApi/StoreApi#cart-api
export const previewCart = {
coupons: [],
shipping_rates: previewShippingRates,
items: [
{
key: '1',
@ -143,9 +146,17 @@ export const previewCart = {
},
},
],
items_count: 4,
items_weight: 0,
needs_shipping: true,
totals: {
currency: 'USD',
currency_code: 'USD',
currency_symbol: '$',
currency_minor_unit: 2,
currency_decimal_separator: '.',
currency_thousand_separator: ',',
currency_prefix: '$',
currency_suffix: '',
total_items: '3000',
total_items_tax: '0',
total_fees: '0',
@ -156,5 +167,6 @@ export const previewCart = {
total_shipping_tax: '0',
total_tax: '0',
total_price: '3000',
tax_lines: [],
},
};

View File

@ -206,4 +206,13 @@
* (true) or not (false).
*/
/**
* @typedef {Object} EditorDataContext
*
* @property {boolean} isEditor Indicates whether in
* the editor context
* (true) or not (false).
* @property {number} currentPostId The post ID being edited.
*/
export {};

View File

@ -18,7 +18,8 @@
"@woocommerce/base-hocs(.*)$": "assets/js/base/hocs/$1",
"@woocommerce/base-hooks(.*)$": "assets/js/base/hooks/$1",
"@woocommerce/base-utils(.*)$": "assets/js/base/utils",
"@woocommerce/block-data": "assets/js/data"
"@woocommerce/block-data": "assets/js/data",
"@woocommerce/resource-previews": "assets/js/previews"
},
"setupFiles": [
"<rootDir>/node_modules/@wordpress/jest-preset-default/scripts/setup-globals.js",