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

View File

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

View File

@ -26,8 +26,14 @@
white-space: pre; 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 { .wc-block-shipping-rates-control__no-results {
margin-bottom: 0; margin-bottom: 0;
font-style: italic;
} }
// Resets when it's inside a panel. // Resets when it's inside a panel.

View File

@ -3,6 +3,7 @@
*/ */
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { isValidElement } from '@wordpress/element';
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount'; 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 ) } className={ classnames( 'wc-block-totals-table-item', className ) }
> >
<span className="wc-block-totals-table-item__label">{ label }</span> <span className="wc-block-totals-table-item__label">{ label }</span>
<FormattedMonetaryAmount { isValidElement( value ) ? (
className="wc-block-totals-table-item__value" <div className="wc-block-totals-table-item__value">
currency={ currency } { value }
displayType="text" </div>
value={ value } ) : (
/> <FormattedMonetaryAmount
<span className="wc-block-totals-table-item__description"> className="wc-block-totals-table-item__value"
currency={ currency }
displayType="text"
value={ value }
/>
) }
<div className="wc-block-totals-table-item__description">
{ description } { description }
</span> </div>
</div> </div>
); );
}; };
TotalsItem.propTypes = { TotalsItem.propTypes = {
currency: PropTypes.object.isRequired, currency: PropTypes.object,
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
value: PropTypes.number.isRequired, value: PropTypes.oneOfType( [ PropTypes.number, PropTypes.node ] ),
className: PropTypes.string, className: PropTypes.string,
description: PropTypes.node, 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 * External dependencies
*/ */
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { import { DISPLAY_CART_PRICES_INCLUDING_TAX } from '@woocommerce/block-settings';
DISPLAY_CART_PRICES_INCLUDING_TAX,
SHIPPING_ENABLED,
} from '@woocommerce/block-settings';
import ShippingCalculator from '@woocommerce/base-components/shipping-calculator'; import ShippingCalculator from '@woocommerce/base-components/shipping-calculator';
import ShippingLocation from '@woocommerce/base-components/shipping-location'; import ShippingLocation from '@woocommerce/base-components/shipping-location';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useState } from '@wordpress/element';
import { useShippingRates } from '@woocommerce/base-hooks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import TotalsItem from '../totals-item'; 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 = ( { const TotalsShippingItem = ( {
currency, currency,
shippingAddress,
updateShippingAddress,
values, values,
showCalculator = true,
showRatesWithoutAddress = false,
} ) => { } ) => {
if ( ! SHIPPING_ENABLED ) { const [ isShippingCalculatorOpen, setIsShippingCalculatorOpen ] = useState(
return null; false
} );
const defaultAddressFields = [ 'country', 'state', 'city', 'postcode' ];
const { const {
total_shipping: totalShipping, shippingRates,
total_shipping_tax: totalShippingTax, shippingAddress,
} = values; shippingRatesLoading,
const shippingValue = parseInt( totalShipping, 10 ); hasShippingAddress,
const shippingTaxValue = parseInt( totalShippingTax, 10 ); 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 ( return (
<TotalsItem <>
currency={ currency } <TotalsItem
description={ label={ __( 'Shipping', 'woo-gutenberg-products-block' ) }
<> value={ totalShippingValue ? totalShippingValue : '' }
{ shippingAddress && ( description={
<ShippingLocation address={ shippingAddress } /> <>
) } <ShippingLocation address={ shippingAddress } />{ ' ' }
{ updateShippingAddress && shippingAddress && ( { showCalculator && (
<ShippingCalculator <button
address={ shippingAddress } className="wc-block-cart__change-address-button"
setAddress={ updateShippingAddress } onClick={ () => {
/> setIsShippingCalculatorOpen(
) } ! isShippingCalculatorOpen
</> );
} } }
label={ __( 'Shipping', 'woo-gutenberg-products-block' ) } >
value={ { __(
DISPLAY_CART_PRICES_INCLUDING_TAX '(change address)',
? shippingValue + shippingTaxValue 'woo-gutenberg-products-block'
: shippingValue ) }
} </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 = { TotalsShippingItem.propTypes = {
currency: PropTypes.object.isRequired, currency: PropTypes.object.isRequired,
shippingAddress: PropTypes.object,
updateShippingAddress: PropTypes.func,
values: PropTypes.shape( { values: PropTypes.shape( {
total_shipping: PropTypes.string, total_shipping: PropTypes.string,
total_shipping_tax: PropTypes.string, total_shipping_tax: PropTypes.string,
} ).isRequired, } ).isRequired,
showCalculator: PropTypes.bool,
showRatesWithoutAddress: PropTypes.bool,
}; };
export default TotalsShippingItem; 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. * Used to dispatch a status update only for the given type.
* *
* @param type * @param {string} type
* *
* @return {Object} The action object. * @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 './product-layout-context';
export * from './query-state-context'; export * from './query-state-context';
export * from './store-notices-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 { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import { useEditorContext } from '@woocommerce/base-context';
import { previewCart } from '@woocommerce/resource-previews';
/** /**
* @constant * @constant
@ -12,6 +14,7 @@ import { useSelect } from '@wordpress/data';
*/ */
const defaultCartData = { const defaultCartData = {
cartCoupons: [], cartCoupons: [],
shippingRates: [],
cartItems: [], cartItems: [],
cartItemsCount: 0, cartItemsCount: 0,
cartItemsWeight: 0, cartItemsWeight: 0,
@ -19,7 +22,6 @@ const defaultCartData = {
cartTotals: {}, cartTotals: {},
cartIsLoading: true, cartIsLoading: true,
cartErrors: [], cartErrors: [],
shippingRates: [],
}; };
/** /**
@ -35,6 +37,7 @@ const defaultCartData = {
* @return {StoreCart} Object containing cart data. * @return {StoreCart} Object containing cart data.
*/ */
export const useStoreCart = ( options = { shouldSelect: true } ) => { export const useStoreCart = ( options = { shouldSelect: true } ) => {
const { isEditor } = useEditorContext();
const { shouldSelect } = options; const { shouldSelect } = options;
const results = useSelect( const results = useSelect(
@ -42,6 +45,21 @@ export const useStoreCart = ( options = { shouldSelect: true } ) => {
if ( ! shouldSelect ) { if ( ! shouldSelect ) {
return null; 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 store = select( storeKey );
const cartData = store.getCartData(); const cartData = store.getCartData();
const cartErrors = store.getCartErrors(); const cartErrors = store.getCartErrors();
@ -49,7 +67,6 @@ export const useStoreCart = ( options = { shouldSelect: true } ) => {
const cartIsLoading = ! store.hasFinishedResolution( const cartIsLoading = ! store.hasFinishedResolution(
'getCartData' 'getCartData'
); );
return { return {
cartCoupons: cartData.coupons, cartCoupons: cartData.coupons,
shippingRates: cartData.shippingRates, shippingRates: cartData.shippingRates,

View File

@ -25,6 +25,7 @@ import { pluckAddress } from '../../utils';
* - {Function} setShippingAddress An function that optimistically * - {Function} setShippingAddress An function that optimistically
* update shipping address and dispatches async rate fetching. * update shipping address and dispatches async rate fetching.
* - {Object} shippingAddress An object containing shipping address. * - {Object} shippingAddress An object containing shipping address.
* - {Object} shippingAddress True when address data exists.
*/ */
export const useShippingRates = ( addressFieldsKeys ) => { export const useShippingRates = ( addressFieldsKeys ) => {
const { cartErrors, shippingRates } = useStoreCart(); const { cartErrors, shippingRates } = useStoreCart();
@ -59,11 +60,13 @@ export const useShippingRates = ( addressFieldsKeys ) => {
updateShippingAddress( debouncedShippingAddress ); updateShippingAddress( debouncedShippingAddress );
} }
}, [ debouncedShippingAddress ] ); }, [ debouncedShippingAddress ] );
return { return {
shippingRates, shippingRates,
shippingAddress, shippingAddress,
setShippingAddress, setShippingAddress,
shippingRatesLoading, shippingRatesLoading,
shippingRatesErrors: cartErrors, 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 PropTypes from 'prop-types';
import { withFeedbackPrompt } from '@woocommerce/block-hocs'; import { withFeedbackPrompt } from '@woocommerce/block-hocs';
import ViewSwitcher from '@woocommerce/block-components/view-switcher'; import ViewSwitcher from '@woocommerce/block-components/view-switcher';
import {
previewCart,
previewShippingRates,
} from '@woocommerce/resource-previews';
import { SHIPPING_ENABLED } from '@woocommerce/block-settings'; import { SHIPPING_ENABLED } from '@woocommerce/block-settings';
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary'; import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
import { EditorProvider } from '@woocommerce/base-context';
/** /**
* Internal dependencies * Internal dependencies
@ -30,10 +27,7 @@ const CartEditor = ( { className, attributes, setAttributes } ) => {
const BlockSettings = () => ( const BlockSettings = () => (
<InspectorControls> <InspectorControls>
<PanelBody <PanelBody
title={ __( title={ __( 'Shipping rates', 'woo-gutenberg-products-block' ) }
'Shipping calculations',
'woo-gutenberg-products-block'
) }
> >
<ToggleControl <ToggleControl
label={ __( label={ __(
@ -41,7 +35,7 @@ const CartEditor = ( { className, attributes, setAttributes } ) => {
'woo-gutenberg-products-block' 'woo-gutenberg-products-block'
) } ) }
help={ __( help={ __(
'Allow customers to estimate shipping.', 'Allow customers to estimate shipping by entering their address.',
'woo-gutenberg-products-block' 'woo-gutenberg-products-block'
) } ) }
checked={ isShippingCalculatorEnabled } checked={ isShippingCalculatorEnabled }
@ -51,24 +45,22 @@ const CartEditor = ( { className, attributes, setAttributes } ) => {
} ) } )
} }
/> />
{ isShippingCalculatorEnabled && ( <ToggleControl
<ToggleControl label={ __(
label={ __( 'Hide shipping costs until an address is entered',
'Hide shipping costs', 'woo-gutenberg-products-block'
'woo-gutenberg-products-block' ) }
) } help={ __(
help={ __( 'If checked, shipping rates will be hidden until the customer uses the shipping calculator or enters their address during checkout.',
'Hide shipping costs until an address is entered.', 'woo-gutenberg-products-block'
'woo-gutenberg-products-block' ) }
) } checked={ isShippingCostHidden }
checked={ isShippingCostHidden } onChange={ () =>
onChange={ () => setAttributes( {
setAttributes( { isShippingCostHidden: ! isShippingCostHidden,
isShippingCostHidden: ! isShippingCostHidden, } )
} ) }
} />
/>
) }
</PanelBody> </PanelBody>
</InspectorControls> </InspectorControls>
); );
@ -112,19 +104,16 @@ const CartEditor = ( { className, attributes, setAttributes } ) => {
) } ) }
> >
<Disabled> <Disabled>
<FullCart <EditorProvider>
cartItems={ previewCart.items } <FullCart
cartTotals={ previewCart.totals } isShippingCostHidden={
isShippingCostHidden={ isShippingCostHidden
isShippingCostHidden }
} isShippingCalculatorEnabled={
isShippingCalculatorEnabled={ isShippingCalculatorEnabled
isShippingCalculatorEnabled }
} />
shippingRates={ </EditorProvider>
previewShippingRates
}
/>
</Disabled> </Disabled>
</BlockErrorBoundary> </BlockErrorBoundary>
</> </>
@ -159,7 +148,7 @@ CartEditor.propTypes = {
export default withFeedbackPrompt( 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' 'woo-gutenberg-products-block'
) )
)( CartEditor ); )( CartEditor );

View File

@ -17,53 +17,69 @@ import { __experimentalCreateInterpolateElement } from 'wordpress-element';
* Internal dependencies * Internal dependencies
*/ */
import FullCart from './full-cart'; import FullCart from './full-cart';
import blockAttributes from './attributes';
import renderFrontend from '../../../utils/render-frontend.js'; 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 = ( { const Block = ( { emptyCart, attributes } ) => {
emptyCart, const { cartItems, cartIsLoading } = useStoreCart();
isShippingCalculatorEnabled,
isShippingCostHidden,
} ) => {
const {
cartItems,
cartTotals,
cartIsLoading,
cartCoupons,
shippingRates,
} = useStoreCart();
return ( return (
<StoreNoticesProvider context="wc/cart"> <>
{ ! cartIsLoading && ! cartItems.length ? ( { ! cartIsLoading && cartItems.length === 0 ? (
<RawHTML>{ emptyCart }</RawHTML> <RawHTML>{ emptyCart }</RawHTML>
) : ( ) : (
<LoadingMask showSpinner={ true } isLoading={ cartIsLoading }> <LoadingMask showSpinner={ true } isLoading={ cartIsLoading }>
<FullCart <FullCart
cartItems={ cartItems }
cartTotals={ cartTotals }
cartCoupons={ cartCoupons }
isShippingCalculatorEnabled={ isShippingCalculatorEnabled={
isShippingCalculatorEnabled attributes.isShippingCalculatorEnabled
} }
isShippingCostHidden={ isShippingCostHidden } isShippingCostHidden={ attributes.isShippingCostHidden }
isLoading={ cartIsLoading }
shippingRates={ shippingRates }
/> />
</LoadingMask> </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> </StoreNoticesProvider>
); );
}; };
const getProps = ( el ) => ( { const getProps = ( el ) => {
emptyCart: el.innerHTML, const attributes = {};
isShippingCalculatorEnabled:
el.dataset.isShippingCalculatorEnabled === 'true', Object.keys( blockAttributes ).forEach( ( key ) => {
isShippingCostHidden: el.dataset.isShippingCostHidden === 'true', 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 = () => { const getErrorBoundaryProps = () => {
return { return {

View File

@ -42,9 +42,6 @@ const getMaximumQuantity = ( backOrdersAllowed, lowStockAmount ) => {
* Cart line item table row component. * Cart line item table row component.
*/ */
const CartLineItemRow = ( { lineItem } ) => { const CartLineItemRow = ( { lineItem } ) => {
/**
* @type {CartItem}
*/
const { const {
name, name,
summary, summary,
@ -142,32 +139,7 @@ const CartLineItemRow = ( { lineItem } ) => {
}; };
CartLineItemRow.propTypes = { CartLineItemRow.propTypes = {
lineItem: PropTypes.shape( { lineItem: PropTypes.object.isRequired,
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,
} ),
}; };
export default CartLineItemRow; export default CartLineItemRow;

View File

@ -10,7 +10,51 @@ import PropTypes from 'prop-types';
import CartLineItemRow from './cart-line-item-row'; import CartLineItemRow from './cart-line-item-row';
const placeholderRows = [ ...Array( 3 ) ].map( ( _x, i ) => ( 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 } ) => { const CartLineItemsTable = ( { lineItems = [], isLoading = false } ) => {

View File

@ -13,17 +13,14 @@ import {
TotalsShippingItem, TotalsShippingItem,
TotalsTaxesItem, TotalsTaxesItem,
} from '@woocommerce/base-components/totals'; } from '@woocommerce/base-components/totals';
import ShippingRatesControl from '@woocommerce/base-components/shipping-rates-control';
import { import {
COUPONS_ENABLED, COUPONS_ENABLED,
SHIPPING_ENABLED,
DISPLAY_CART_PRICES_INCLUDING_TAX, DISPLAY_CART_PRICES_INCLUDING_TAX,
SHIPPING_ENABLED,
} from '@woocommerce/block-settings'; } from '@woocommerce/block-settings';
import { getCurrencyFromPriceResponse } from '@woocommerce/base-utils'; import { getCurrencyFromPriceResponse } from '@woocommerce/base-utils';
import { Card, CardBody } from 'wordpress-components'; import { Card, CardBody } from 'wordpress-components';
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount'; import { useStoreCartCoupons, useStoreCart } from '@woocommerce/base-hooks';
import { decodeEntities } from '@wordpress/html-entities';
import { useStoreCartCoupons, useShippingRates } from '@woocommerce/base-hooks';
import classnames from 'classnames'; import classnames from 'classnames';
import { import {
Sidebar, Sidebar,
@ -41,85 +38,30 @@ import CartLineItemsTable from './cart-line-items-table';
import './style.scss'; import './style.scss';
import './editor.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". * Component that renders the Cart block when user has something in cart aka "full".
*/ */
const Cart = ( { const Cart = ( { isShippingCalculatorEnabled, isShippingCostHidden } ) => {
cartItems = [], const { cartItems, cartTotals, cartIsLoading, cartErrors } = useStoreCart();
cartTotals = {},
cartCoupons = [],
isShippingCalculatorEnabled,
isShippingCostHidden,
shippingRates,
isLoading = false,
} ) => {
const defaultAddressFields = [ 'country', 'state', 'city', 'postcode' ];
const {
shippingAddress,
setShippingAddress,
shippingRatesLoading,
} = useShippingRates( defaultAddressFields );
const { const {
applyCoupon, applyCoupon,
removeCoupon, removeCoupon,
isApplyingCoupon, isApplyingCoupon,
isRemovingCoupon, isRemovingCoupon,
cartCoupons,
cartCouponsErrors,
} = useStoreCartCoupons(); } = useStoreCartCoupons();
const showShippingCosts = Boolean( const errors = [ ...cartErrors, ...cartCouponsErrors ];
SHIPPING_ENABLED && if ( errors.length > 0 ) {
isShippingCalculatorEnabled && throw new Error( errors[ 0 ].message );
( ! isShippingCostHidden || shippingAddress?.country ) }
);
const totalsCurrency = getCurrencyFromPriceResponse( cartTotals ); const totalsCurrency = getCurrencyFromPriceResponse( cartTotals );
const cartClassName = classnames( 'wc-block-cart', { const cartClassName = classnames( 'wc-block-cart', {
'wc-block-cart--is-loading': isLoading, 'wc-block-cart--is-loading': cartIsLoading,
} ); } );
return ( return (
@ -128,7 +70,7 @@ const Cart = ( {
<CartLineItemsTitle itemCount={ cartItems.length } /> <CartLineItemsTitle itemCount={ cartItems.length } />
<CartLineItemsTable <CartLineItemsTable
lineItems={ cartItems } lineItems={ cartItems }
isLoading={ isLoading } isLoading={ cartIsLoading }
/> />
</Main> </Main>
<Sidebar className="wc-block-cart__sidebar"> <Sidebar className="wc-block-cart__sidebar">
@ -155,30 +97,16 @@ const Cart = ( {
removeCoupon={ removeCoupon } removeCoupon={ removeCoupon }
values={ cartTotals } values={ cartTotals }
/> />
{ isShippingCalculatorEnabled && ( { SHIPPING_ENABLED && (
<TotalsShippingItem <TotalsShippingItem
currency={ totalsCurrency } showCalculator={ isShippingCalculatorEnabled }
shippingAddress={ shippingAddress } showRatesWithoutAddress={
updateShippingAddress={ setShippingAddress } ! isShippingCostHidden
}
values={ cartTotals } 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 && ( { ! DISPLAY_CART_PRICES_INCLUDING_TAX && (
<TotalsTaxesItem <TotalsTaxesItem
currency={ totalsCurrency } currency={ totalsCurrency }
@ -188,7 +116,6 @@ const Cart = ( {
{ COUPONS_ENABLED && ( { COUPONS_ENABLED && (
<TotalsCouponCodeInput <TotalsCouponCodeInput
onSubmit={ applyCoupon } onSubmit={ applyCoupon }
initialOpen={ true }
isLoading={ isApplyingCoupon } isLoading={ isApplyingCoupon }
/> />
) } ) }
@ -205,26 +132,8 @@ const Cart = ( {
}; };
Cart.propTypes = { 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, isShippingCalculatorEnabled: PropTypes.bool,
isShippingCostHidden: 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; export default Cart;

View File

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

View File

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

View File

@ -232,7 +232,7 @@ const CheckoutEditor = ( { attributes, setAttributes } ) => {
export default withFeedbackPrompt( 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' 'woo-gutenberg-products-block'
) )
)( CheckoutEditor ); )( CheckoutEditor );

View File

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

View File

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

View File

@ -206,4 +206,13 @@
* (true) or not (false). * (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 {}; export {};

View File

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