Create Context Provider for Notices w/ Notices API (https://github.com/woocommerce/woocommerce-blocks/pull/1843)

* Working on store provider

* Working on store provider

* Reducer implementation

* Implement core/notices

* Add notices to store coupon hook with context

* Improve store notice text and styling

* Improve JS side API for notices

* Wrap functions with context additon

* Update test to []

* Implement props feedback and useInstanceId

* Update assets/js/base/context/store-notices-context.js

Co-Authored-By: Darren Ethier <darren@roughsmootheng.in>

* Update assets/js/base/context/store-notices-context.js

Co-Authored-By: Darren Ethier <darren@roughsmootheng.in>

* remove instance id

Co-authored-by: Darren Ethier <darren@roughsmootheng.in>
This commit is contained in:
Mike Jolley 2020-03-03 10:26:02 +00:00 committed by GitHub
parent 5fcf9b0fca
commit d3a9dc3d6b
14 changed files with 397 additions and 41 deletions

View File

@ -0,0 +1,59 @@
/**
* External dependencies
*/
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Notice } from 'wordpress-components';
import { useStoreNoticesContext } from '@woocommerce/base-context/store-notices-context';
/**
* Internal dependencies
*/
import './style.scss';
const getWooClassName = ( { status = 'default' } ) => {
switch ( status ) {
case 'error':
return 'woocommerce-message woocommerce-error';
case 'success':
return 'woocommerce-message woocommerce-success';
case 'info':
case 'warning':
return 'woocommerce-message woocommerce-info';
}
return '';
};
const StoreNoticesContainer = ( { className, notices } ) => {
const { removeNotice } = useStoreNoticesContext();
const wrapperClass = classnames( className, 'wc-block-components-notices' );
return (
<div className={ wrapperClass }>
{ notices.map( ( props ) => (
<Notice
key={ 'store-notice-' + props.id }
{ ...props }
className={ classnames(
'wc-block-components-notices__notice',
getWooClassName( props )
) }
onRemove={ () => {
if ( props.isDismissible ) {
removeNotice( props.id );
}
} }
>
{ props.content }
</Notice>
) ) }
</div>
);
};
StoreNoticesContainer.propTypes = {
className: PropTypes.string,
notices: PropTypes.array,
};
export default StoreNoticesContainer;

View File

@ -0,0 +1,26 @@
.wc-block-components-notices {
display: block;
margin-bottom: 2em;
.wc-block-components-notices__notice {
margin: 0;
.components-notice__content {
display: inline-block;
}
.components-notice__dismiss {
background: transparent none;
padding: 0;
margin: 0;
border: 0;
outline: 0;
color: #fff;
float: right;
svg {
fill: #fff;
vertical-align: text-top;
}
}
}
.wc-block-components-notices__notice + .wc-block-components-notices__notice {
margin-top: 1em;
}
}

View File

@ -0,0 +1,91 @@
/**
* External dependencies
*/
import PropTypes from 'prop-types';
import { createContext, useContext, useCallback } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import StoreNoticesContainer from '@woocommerce/base-components/store-notices-container';
const StoreNoticesContext = createContext( {
notices: [],
createNotice: () => void null,
removeNotice: () => void null,
context: 'wc/core',
} );
export const useStoreNoticesContext = () => {
return useContext( StoreNoticesContext );
};
/**
* Provides an interface for blocks to add notices to the frontend UI.
*
* Statuses map to https://github.com/WordPress/gutenberg/tree/master/packages/components/src/notice
* - Default (no status)
* - Error
* - Warning
* - Info
* - Success
*/
const StoreNoticesProvider = ( {
children,
className = '',
createNoticeContainer = true,
context = 'wc/core',
} ) => {
const { createNotice, removeNotice } = useDispatch( 'core/notices' );
const createNoticeWithContext = useCallback(
( status = 'default', content = '', options = {} ) => {
createNotice( status, content, {
...options,
context: options.context || context,
} );
},
[ createNotice, context ]
);
const removeNoticeWithContext = useCallback(
( id ) => {
removeNotice( id, context );
},
[ createNotice, context ]
);
const { notices } = useSelect(
( select ) => {
return {
notices: select( 'core/notices' ).getNotices( context ),
};
},
[ context ]
);
const contextValue = {
notices,
createNotice: createNoticeWithContext,
removeNotice: removeNoticeWithContext,
context,
};
return (
<StoreNoticesContext.Provider value={ contextValue }>
{ createNoticeContainer && (
<StoreNoticesContainer
className={ className }
notices={ contextValue.notices }
/>
) }
{ children }
</StoreNoticesContext.Provider>
);
};
StoreNoticesProvider.propTypes = {
className: PropTypes.string,
createNoticeContainer: PropTypes.bool,
children: PropTypes.node,
context: PropTypes.string,
};
export default StoreNoticesProvider;

View File

@ -4,6 +4,7 @@ export * from './use-store-cart';
export * from './use-store-cart-coupons';
export * from './use-store-cart-item';
export * from './use-store-products';
export * from './use-store-notices';
export * from './use-collection';
export * from './use-collection-header';
export * from './use-collection-data';

View File

@ -3,8 +3,10 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { useStoreNotices } from '@woocommerce/base-hooks';
/**
* Internal dependencies
@ -21,20 +23,80 @@ import { useStoreCart } from './use-store-cart';
*/
export const useStoreCartCoupons = () => {
const { cartCoupons, cartIsLoading } = useStoreCart();
const {
addErrorNotice,
addSuccessNotice,
addInfoNotice,
} = useStoreNotices();
const results = useSelect( ( select, { dispatch } ) => {
const store = select( storeKey );
const isApplyingCoupon = store.isApplyingCoupon();
const isRemovingCoupon = store.isRemovingCoupon();
const { applyCoupon, removeCoupon } = dispatch( storeKey );
const results = useSelect(
( select, { dispatch } ) => {
const store = select( storeKey );
const isApplyingCoupon = store.isApplyingCoupon();
const isRemovingCoupon = store.isRemovingCoupon();
const { applyCoupon, removeCoupon } = dispatch( storeKey );
return {
applyCoupon,
removeCoupon,
isApplyingCoupon,
isRemovingCoupon,
};
}, [] );
const applyCouponWithNotices = ( couponCode ) => {
applyCoupon( couponCode )
.then( ( result ) => {
if ( result === true ) {
addSuccessNotice(
sprintf(
// translators: %s coupon code.
__(
'Coupon code "%s" has been applied to your cart',
'woo-gutenberg-products-block'
),
couponCode
),
{
id: 'coupon-form',
}
);
}
} )
.catch( ( error ) => {
addErrorNotice( error.message, {
id: 'coupon-form',
} );
} );
};
const removeCouponWithNotices = ( couponCode ) => {
removeCoupon( couponCode )
.then( ( result ) => {
if ( result === true ) {
addInfoNotice(
sprintf(
// translators: %s coupon code.
__(
'Coupon code "%s" has been removed from your cart',
'woo-gutenberg-products-block'
),
couponCode
),
{
id: 'coupon-form',
}
);
}
} )
.catch( ( error ) => {
addErrorNotice( error.message, {
id: 'coupon-form',
} );
} );
};
return {
applyCoupon: applyCouponWithNotices,
removeCoupon: removeCouponWithNotices,
isApplyingCoupon,
isRemovingCoupon,
};
},
[ addErrorNotice, addSuccessNotice, addInfoNotice ]
);
return {
appliedCoupons: cartCoupons,

View File

@ -0,0 +1,48 @@
/**
* External dependencies
*/
import { useStoreNoticesContext } from '@woocommerce/base-context/store-notices-context';
import { useMemo } from '@wordpress/element';
export const useStoreNotices = () => {
const { notices, createNotice, removeNotice } = useStoreNoticesContext();
const noticesApi = useMemo(
() => ( {
addDefaultNotice: ( text, noticeProps = {} ) =>
void createNotice( 'default', text, {
...noticeProps,
} ),
addErrorNotice: ( text, noticeProps = {} ) =>
void createNotice( 'error', text, {
...noticeProps,
} ),
addWarningNotice: ( text, noticeProps = {} ) =>
void createNotice( 'warning', text, {
...noticeProps,
} ),
addInfoNotice: ( text, noticeProps = {} ) =>
void createNotice( 'info', text, {
...noticeProps,
} ),
addSuccessNotice: ( text, noticeProps = {} ) =>
void createNotice( 'success', text, {
...noticeProps,
} ),
removeNotices: ( type = null ) => {
notices.map( ( notice ) => {
if ( type === null || notice.status === type ) {
removeNotice( notice.id );
}
return true;
} );
},
} ),
[ createNotice ]
);
return {
notices,
...noticesApi,
};
};

View File

@ -5,6 +5,8 @@ import { withRestApiHydration } from '@woocommerce/block-hocs';
import { useStoreCart } from '@woocommerce/base-hooks';
import { RawHTML } from '@wordpress/element';
import LoadingMask from '@woocommerce/base-components/loading-mask';
import StoreNoticesProvider from '@woocommerce/base-context/store-notices-context';
/**
* Internal dependencies
*/
@ -23,21 +25,11 @@ const CartFrontend = ( {
cartItems,
cartTotals,
cartIsLoading,
cartErrors,
cartCoupons,
} = useStoreCart();
return (
<>
<div className="errors">
{ // @todo This is a placeholder for error messages - this needs refactoring.
cartErrors &&
cartErrors.map( ( error = {}, i ) => (
<div className="woocommerce-info" key={ 'notice-' + i }>
{ error.message }
</div>
) ) }
</div>
<StoreNoticesProvider context="wc/cart">
{ ! cartIsLoading && ! cartItems.length ? (
<RawHTML>{ emptyCart }</RawHTML>
) : (
@ -54,7 +46,7 @@ const CartFrontend = ( {
/>
</LoadingMask>
) }
</>
</StoreNoticesProvider>
);
};

View File

@ -72,6 +72,7 @@ export function receiveRemovingCoupon( couponCode ) {
* Applies a coupon code and either invalidates caches, or receives an error if
* the coupon cannot be applied.
*
* @throws Will throw an error if there is an API problem.
* @param {string} couponCode The coupon code to apply to the cart.
*/
export function* applyCoupon( couponCode ) {
@ -90,17 +91,26 @@ export function* applyCoupon( couponCode ) {
if ( result ) {
yield receiveCart( result );
}
// Finished handling the coupon.
yield receiveApplyingCoupon( '' );
} catch ( error ) {
// Store the error message in state.
yield receiveError( error );
// Finished handling the coupon.
yield receiveApplyingCoupon( '' );
// Re-throw the error.
throw error;
}
yield receiveApplyingCoupon( '' );
return true;
}
/**
* Removes a coupon code and either invalidates caches, or receives an error if
* the coupon cannot be removed.
*
* @throws Will throw an error if there is an API problem.
* @param {string} couponCode The coupon code to remove from the cart.
*/
export function* removeCoupon( couponCode ) {
@ -119,11 +129,19 @@ export function* removeCoupon( couponCode ) {
if ( result ) {
yield receiveCart( result );
}
// Finished handling the coupon.
yield receiveRemovingCoupon( '' );
} catch ( error ) {
// Store the error message in state.
yield receiveError( error );
// Finished handling the coupon.
yield receiveRemovingCoupon( '' );
// Re-throw the error.
throw error;
}
yield receiveRemovingCoupon( '' );
return true;
}
/**

View File

@ -7,13 +7,9 @@ import { addQueryArgs } from '@wordpress/url';
/**
* Internal dependencies
*/
import {
receiveCollection,
receiveCollectionError,
DEFAULT_EMPTY_ARRAY,
} from './actions';
import { receiveCollection, receiveCollectionError } from './actions';
import { STORE_KEY as SCHEMA_STORE_KEY } from '../schema/constants';
import { STORE_KEY } from './constants';
import { STORE_KEY, DEFAULT_EMPTY_ARRAY } from './constants';
import { apiFetchWithHeaders } from './controls';
/**

View File

@ -88,7 +88,7 @@ describe( 'getCollection', () => {
'products',
'?foo=bar',
[ 20, 30 ],
{ items: undefined, headers: undefined }
{ items: [], headers: undefined }
)
);
}

View File

@ -1,3 +1,11 @@
/**
* External dependencies
*/
import '@wordpress/notices';
/**
* Internal dependencies
*/
export { SCHEMA_STORE_KEY } from './schema';
export { COLLECTIONS_STORE_KEY } from './collections';
export { CART_STORE_KEY } from './cart';

View File

@ -5100,6 +5100,18 @@
}
}
},
"@wordpress/notices": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/@wordpress/notices/-/notices-1.12.0.tgz",
"integrity": "sha512-TSX9ih2LfInO+/v0lb1k1PBOHYveIKINkLAmD+BJtAgFVjbJG1465rinv+efAYiqcnmQhrHHrpn4wGUP/7c0jg==",
"dev": true,
"requires": {
"@babel/runtime": "^7.8.3",
"@wordpress/a11y": "^2.7.0",
"@wordpress/data": "^4.13.0",
"lodash": "^4.17.15"
}
},
"@wordpress/viewport": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@wordpress/viewport/-/viewport-2.13.0.tgz",
@ -5409,14 +5421,13 @@
}
},
"@wordpress/notices": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/@wordpress/notices/-/notices-1.12.0.tgz",
"integrity": "sha512-TSX9ih2LfInO+/v0lb1k1PBOHYveIKINkLAmD+BJtAgFVjbJG1465rinv+efAYiqcnmQhrHHrpn4wGUP/7c0jg==",
"dev": true,
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@wordpress/notices/-/notices-2.0.0.tgz",
"integrity": "sha512-NOkI8r2YLRxRrx+z7wDzpifyJAMnKezVjTnsy6EiU84Kai7FM2Ce+I51asw3c6UfLPpDY8DjLrzUWVZkhQF/og==",
"requires": {
"@babel/runtime": "^7.8.3",
"@wordpress/a11y": "^2.7.0",
"@wordpress/data": "^4.13.0",
"@wordpress/data": "^4.14.0",
"lodash": "^4.17.15"
}
},
@ -29654,6 +29665,40 @@
}
}
},
"wordpress-compose": {
"version": "npm:@wordpress/compose@3.11.0",
"resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-3.11.0.tgz",
"integrity": "sha512-CNbLn9NtG2A0X71wjEux126uEHpWp3v546FtSgMoWlq73z3LEEBDoEeS2glIPAbIK6e1X2UibsKrn5Tn651tlg==",
"requires": {
"@babel/runtime": "^7.8.3",
"@wordpress/element": "^2.11.0",
"@wordpress/is-shallow-equal": "^1.8.0",
"lodash": "^4.17.15",
"mousetrap": "^1.6.2"
},
"dependencies": {
"@wordpress/element": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/@wordpress/element/-/element-2.11.0.tgz",
"integrity": "sha512-56ZO8a+E7QEsYwiqS+3BQPSHrCPsOAIEz5smXzntb2f6BjvOKeA64pup40mdn1pNGexe06LBA8cjoZVdLBHB1w==",
"requires": {
"@babel/runtime": "^7.8.3",
"@wordpress/escape-html": "^1.7.0",
"lodash": "^4.17.15",
"react": "^16.9.0",
"react-dom": "^16.9.0"
}
},
"@wordpress/is-shallow-equal": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-1.8.0.tgz",
"integrity": "sha512-OV3qJqP9LhjuOzt85TsyBwv+//CvC8Byf/81D3NmjPKlstLaD/bBCC5nBhH6dKAv4bShYtQ2Hmut+V4dZnOM1A==",
"requires": {
"@babel/runtime": "^7.8.3"
}
}
}
},
"wordpress-element": {
"version": "npm:@wordpress/element@2.11.0",
"resolved": "https://registry.npmjs.org/@wordpress/element/-/element-2.11.0.tgz",

View File

@ -141,6 +141,7 @@
},
"dependencies": {
"@woocommerce/components": "4.0.0",
"@wordpress/notices": "^2.0.0",
"classnames": "2.2.6",
"compare-versions": "3.6.0",
"config": "3.2.6",
@ -149,7 +150,8 @@
"trim-html": "0.1.9",
"use-debounce": "3.3.0",
"wordpress-components": "npm:@wordpress/components@8.5.0",
"wordpress-element": "npm:@wordpress/element@2.11.0"
"wordpress-element": "npm:@wordpress/element@2.11.0",
"wordpress-compose": "npm:@wordpress/compose@3.11.0"
},
"husky": {
"hooks": {

View File

@ -228,7 +228,11 @@ class CartController {
if ( $coupon->get_code() !== $coupon_code ) {
throw new RestException(
'woocommerce_rest_cart_coupon_error',
__( 'Invalid coupon code.', 'woo-gutenberg-products-block' ),
sprintf(
/* Translators: %s coupon code */
__( '"%s" is an invalid coupon code.', 'woo-gutenberg-products-block' ),
esc_html( $coupon_code )
),
403
);
}
@ -236,7 +240,11 @@ class CartController {
if ( $this->has_coupon( $coupon_code ) ) {
throw new RestException(
'woocommerce_rest_cart_coupon_error',
__( 'Coupon has already been applied.', 'woo-gutenberg-products-block' ),
sprintf(
/* Translators: %s coupon code */
__( 'Coupon code "%s" has already been applied.', 'woo-gutenberg-products-block' ),
esc_html( $coupon_code )
),
403
);
}