Expose cart errors as notices (https://github.com/woocommerce/woocommerce-blocks/pull/8162)
* Scroll to errors when interacting with radio buttons * Fix full stop wrapping in checkout * Make type guard for api response reusable * Merge useShippingData and useSelectShippingRate Overlapping functionality and responsibility easily merged into a single hook. * ShippingDataProvider Typescript * Create errors when receiving errors via thunk * Update DEFAULT_ERROR_MESSAGE * Update tests since all errors are set via new action * Correct SET_ERROR_DATA * Update json error text and allow it to be dismissed * Add back missing comment in types * Put back typedef * Allow Store Notice Containers to display subContexts without changing original context * receiveError handles cart * Update assets/js/data/cart/notify-errors.ts Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com> * Update assets/js/base/context/hooks/shipping/types.ts Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com> * Remove debug * Revise type (remove as) * rename to unregisteredSubContexts * getNoticeContexts comment * Add test for unregistered errors * Update comment Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com>
This commit is contained in:
parent
c86ea1aa66
commit
435e341fe6
|
@ -82,6 +82,7 @@
|
|||
top: 0;
|
||||
text-align: center;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
|
||||
.is-mobile &,
|
||||
.is-small & {
|
||||
|
|
|
@ -7,7 +7,10 @@ import { decodeEntities } from '@wordpress/html-entities';
|
|||
import type { ReactElement } from 'react';
|
||||
import { Panel } from '@woocommerce/blocks-checkout';
|
||||
import Label from '@woocommerce/base-components/label';
|
||||
import { useShippingData } from '@woocommerce/base-context/hooks';
|
||||
import {
|
||||
useShippingData,
|
||||
useStoreEvents,
|
||||
} from '@woocommerce/base-context/hooks';
|
||||
import { sanitizeHTML } from '@woocommerce/utils';
|
||||
|
||||
/**
|
||||
|
@ -27,6 +30,7 @@ export const ShippingRatesControlPackage = ( {
|
|||
showItems,
|
||||
}: PackageProps ): ReactElement => {
|
||||
const { selectShippingRate } = useShippingData();
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
const multiplePackages =
|
||||
document.querySelectorAll(
|
||||
'.wc-block-components-shipping-rates-control__package'
|
||||
|
@ -90,8 +94,12 @@ export const ShippingRatesControlPackage = ( {
|
|||
className,
|
||||
noResultsMessage,
|
||||
rates: packageData.shipping_rates,
|
||||
onSelectRate: ( newShippingRateId: string ) =>
|
||||
selectShippingRate( newShippingRateId, packageId ),
|
||||
onSelectRate: ( newShippingRateId: string ) => {
|
||||
selectShippingRate( newShippingRateId, packageId );
|
||||
dispatchCheckoutEvent( 'set-selected-shipping-rate', {
|
||||
shippingRateId: newShippingRateId,
|
||||
} );
|
||||
},
|
||||
selectedRate: packageData.shipping_rates.find(
|
||||
( rate ) => rate.selected
|
||||
),
|
||||
|
|
|
@ -2,23 +2,20 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { Cart } from '@woocommerce/type-defs/cart';
|
||||
import { SelectShippingRateType } from '@woocommerce/type-defs/shipping';
|
||||
|
||||
export interface ShippingData extends SelectShippingRateType {
|
||||
export interface ShippingData {
|
||||
needsShipping: Cart[ 'needsShipping' ];
|
||||
hasCalculatedShipping: Cart[ 'hasCalculatedShipping' ];
|
||||
shippingRates: Cart[ 'shippingRates' ];
|
||||
isLoadingRates: boolean;
|
||||
selectedRates: Record< string, string | unknown >;
|
||||
|
||||
/**
|
||||
* The following values are used to determine if pickup methods are shown separately from shipping methods, or if
|
||||
* those options should be hidden.
|
||||
*/
|
||||
|
||||
// Returns a function that accepts a shipping rate ID and a package ID.
|
||||
selectShippingRate: (
|
||||
newShippingRateId: string,
|
||||
packageId: string | number
|
||||
) => void;
|
||||
// Only true when ALL packages support local pickup. If true, we can show the collection/delivery toggle
|
||||
isCollectable: boolean;
|
||||
|
||||
// True when at least one package has selected local pickup
|
||||
hasSelectedLocalPickup: boolean;
|
||||
// True when a rate is currently being selected and persisted to the server.
|
||||
isSelectingRate: boolean;
|
||||
}
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { useCallback } from '@wordpress/element';
|
||||
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
import { useThrowError } from '@woocommerce/base-hooks';
|
||||
import { SelectShippingRateType } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useStoreEvents } from '../use-store-events';
|
||||
|
||||
/**
|
||||
* This is a custom hook for selecting shipping rates for a shipping package.
|
||||
*
|
||||
* @return {Object} This hook will return an object with these properties:
|
||||
* - selectShippingRate: A function that immediately returns the selected rate and dispatches an action generator.
|
||||
* - isSelectingRate: True when rates are being resolved to the API.
|
||||
*/
|
||||
export const useSelectShippingRate = (): SelectShippingRateType => {
|
||||
const throwError = useThrowError();
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
|
||||
const { selectShippingRate: dispatchSelectShippingRate } = useDispatch(
|
||||
storeKey
|
||||
) as {
|
||||
selectShippingRate: unknown;
|
||||
} as {
|
||||
selectShippingRate: (
|
||||
newShippingRateId: string,
|
||||
packageId?: string | number
|
||||
) => Promise< unknown >;
|
||||
};
|
||||
|
||||
// Selects a shipping rate, fires an event, and catch any errors.
|
||||
const selectShippingRate = useCallback(
|
||||
( newShippingRateId, packageId ) => {
|
||||
dispatchSelectShippingRate( newShippingRateId, packageId )
|
||||
.then( () => {
|
||||
dispatchCheckoutEvent( 'set-selected-shipping-rate', {
|
||||
shippingRateId: newShippingRateId,
|
||||
} );
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
// Throw an error because an error when selecting a rate is problematic.
|
||||
throwError( error );
|
||||
} );
|
||||
},
|
||||
[ dispatchSelectShippingRate, dispatchCheckoutEvent, throwError ]
|
||||
);
|
||||
|
||||
// See if rates are being selected.
|
||||
const isSelectingRate = useSelect< boolean >( ( select ) => {
|
||||
return select( storeKey ).isShippingRateBeingSelected();
|
||||
}, [] );
|
||||
|
||||
return {
|
||||
selectShippingRate,
|
||||
isSelectingRate,
|
||||
};
|
||||
};
|
|
@ -23,6 +23,7 @@ export const useShippingData = (): ShippingData => {
|
|||
hasCalculatedShipping,
|
||||
isLoadingRates,
|
||||
isCollectable,
|
||||
isSelectingRate,
|
||||
} = useSelect( ( select ) => {
|
||||
const isEditor = !! select( 'core/editor' );
|
||||
const store = select( storeKey );
|
||||
|
@ -45,14 +46,12 @@ export const useShippingData = (): ShippingData => {
|
|||
methodId === 'pickup_location'
|
||||
)
|
||||
),
|
||||
isSelectingRate: isEditor
|
||||
? false
|
||||
: store.isShippingRateBeingSelected(),
|
||||
};
|
||||
} );
|
||||
|
||||
// See if rates are being selected.
|
||||
const isSelectingRate = useSelect< boolean >( ( select ) => {
|
||||
return select( storeKey ).isShippingRateBeingSelected();
|
||||
}, [] );
|
||||
|
||||
// set selected rates on ref so it's always current.
|
||||
const selectedRates = useRef< Record< string, string > >( {} );
|
||||
useEffect( () => {
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
/**
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').ShippingErrorTypes} ShippingErrorTypes
|
||||
* @typedef {import('@woocommerce/type-defs/shipping').ShippingAddress} ShippingAddress
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').ShippingDataContext} ShippingDataContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {ShippingErrorTypes}
|
||||
*/
|
||||
export const ERROR_TYPES = {
|
||||
NONE: 'none',
|
||||
INVALID_ADDRESS: 'invalid_address',
|
||||
UNKNOWN: 'unknown_error',
|
||||
};
|
||||
|
||||
export const shippingErrorCodes = {
|
||||
INVALID_COUNTRY: 'woocommerce_rest_cart_shipping_rates_invalid_country',
|
||||
MISSING_COUNTRY: 'woocommerce_rest_cart_shipping_rates_missing_country',
|
||||
INVALID_STATE: 'woocommerce_rest_cart_shipping_rates_invalid_state',
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {ShippingAddress}
|
||||
*/
|
||||
export const DEFAULT_SHIPPING_ADDRESS = {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
company: '',
|
||||
address_1: '',
|
||||
address_2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postcode: '',
|
||||
country: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {ShippingDataContext}
|
||||
*/
|
||||
export const DEFAULT_SHIPPING_CONTEXT_DATA = {
|
||||
shippingErrorStatus: {
|
||||
isPristine: true,
|
||||
isValid: false,
|
||||
hasInvalidAddress: false,
|
||||
hasError: false,
|
||||
},
|
||||
dispatchErrorStatus: () => null,
|
||||
shippingErrorTypes: ERROR_TYPES,
|
||||
shippingRates: [],
|
||||
isLoadingRates: false,
|
||||
selectedRates: [],
|
||||
setSelectedRates: () => null,
|
||||
shippingAddress: DEFAULT_SHIPPING_ADDRESS,
|
||||
setShippingAddress: () => null,
|
||||
onShippingRateSuccess: () => null,
|
||||
onShippingRateFail: () => null,
|
||||
onShippingRateSelectSuccess: () => null,
|
||||
onShippingRateSelectFail: () => null,
|
||||
needsShipping: false,
|
||||
};
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { CartShippingAddress } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { ShippingDataContextType, ShippingErrorTypes } from './types';
|
||||
|
||||
export const ERROR_TYPES = {
|
||||
NONE: 'none',
|
||||
INVALID_ADDRESS: 'invalid_address',
|
||||
UNKNOWN: 'unknown_error',
|
||||
} as ShippingErrorTypes;
|
||||
|
||||
export const shippingErrorCodes = {
|
||||
INVALID_COUNTRY: 'woocommerce_rest_cart_shipping_rates_invalid_country',
|
||||
MISSING_COUNTRY: 'woocommerce_rest_cart_shipping_rates_missing_country',
|
||||
INVALID_STATE: 'woocommerce_rest_cart_shipping_rates_invalid_state',
|
||||
};
|
||||
|
||||
export const DEFAULT_SHIPPING_ADDRESS = {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
company: '',
|
||||
address_1: '',
|
||||
address_2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postcode: '',
|
||||
country: '',
|
||||
} as CartShippingAddress;
|
||||
|
||||
export const DEFAULT_SHIPPING_CONTEXT_DATA = {
|
||||
shippingErrorStatus: {
|
||||
isPristine: true,
|
||||
isValid: false,
|
||||
hasInvalidAddress: false,
|
||||
hasError: false,
|
||||
},
|
||||
dispatchErrorStatus: ( status ) => status,
|
||||
shippingErrorTypes: ERROR_TYPES,
|
||||
onShippingRateSuccess: () => () => void null,
|
||||
onShippingRateFail: () => () => void null,
|
||||
onShippingRateSelectSuccess: () => () => void null,
|
||||
onShippingRateSelectFail: () => () => void null,
|
||||
} as ShippingDataContextType;
|
|
@ -15,6 +15,10 @@ import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type {
|
||||
ShippingDataContextType,
|
||||
ShippingDataProviderProps,
|
||||
} from './types';
|
||||
import { ERROR_TYPES, DEFAULT_SHIPPING_CONTEXT_DATA } from './constants';
|
||||
import { hasInvalidShippingAddress } from './utils';
|
||||
import { errorStatusReducer } from './reducers';
|
||||
|
@ -27,28 +31,19 @@ import {
|
|||
import { useStoreCart } from '../../../hooks/cart/use-store-cart';
|
||||
import { useShippingData } from '../../../hooks/shipping/use-shipping-data';
|
||||
|
||||
/**
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').ShippingDataContext} ShippingDataContext
|
||||
* @typedef {import('react')} React
|
||||
*/
|
||||
|
||||
const { NONE, INVALID_ADDRESS, UNKNOWN } = ERROR_TYPES;
|
||||
const ShippingDataContext = createContext( DEFAULT_SHIPPING_CONTEXT_DATA );
|
||||
|
||||
/**
|
||||
* @return {ShippingDataContext} Returns data and functions related to shipping methods.
|
||||
*/
|
||||
export const useShippingDataContext = () => {
|
||||
export const useShippingDataContext = (): ShippingDataContextType => {
|
||||
return useContext( ShippingDataContext );
|
||||
};
|
||||
|
||||
/**
|
||||
* The shipping data provider exposes the interface for shipping in the checkout/cart.
|
||||
*
|
||||
* @param {Object} props Incoming props for provider
|
||||
* @param {React.ReactElement} props.children
|
||||
*/
|
||||
export const ShippingDataProvider = ( { children } ) => {
|
||||
export const ShippingDataProvider = ( {
|
||||
children,
|
||||
}: ShippingDataProviderProps ) => {
|
||||
const { __internalIncrementCalculating, __internalDecrementCalculating } =
|
||||
useDispatch( CHECKOUT_STORE_KEY );
|
||||
const { shippingRates, isLoadingRates, cartErrors } = useStoreCart();
|
||||
|
@ -191,10 +186,7 @@ export const ShippingDataProvider = ( { children } ) => {
|
|||
currentErrorStatus.hasInvalidAddress,
|
||||
] );
|
||||
|
||||
/**
|
||||
* @type {ShippingDataContext}
|
||||
*/
|
||||
const ShippingData = {
|
||||
const ShippingData: ShippingDataContextType = {
|
||||
shippingErrorStatus: currentErrorStatus,
|
||||
dispatchErrorStatus,
|
||||
shippingErrorTypes: ERROR_TYPES,
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { emitterCallback } from '../../../event-emit';
|
||||
|
||||
export type ShippingErrorStatus = {
|
||||
isPristine: boolean;
|
||||
isValid: boolean;
|
||||
hasInvalidAddress: boolean;
|
||||
hasError: boolean;
|
||||
};
|
||||
|
||||
export type ShippingErrorTypes = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
NONE: 'none';
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
INVALID_ADDRESS: 'invalid_address';
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
UNKNOWN: 'unknown_error';
|
||||
};
|
||||
|
||||
export type ShippingDataContextType = {
|
||||
// A function for dispatching a shipping rate error status.
|
||||
dispatchErrorStatus: React.Dispatch< {
|
||||
type: string;
|
||||
} >;
|
||||
onShippingRateFail: ReturnType< typeof emitterCallback >;
|
||||
// Used to register a callback to be invoked when shipping rate is selected unsuccessfully
|
||||
onShippingRateSelectFail: ReturnType< typeof emitterCallback >;
|
||||
// Used to register a callback to be invoked when shipping rate is selected.
|
||||
onShippingRateSelectSuccess: ReturnType< typeof emitterCallback >;
|
||||
// Used to register a callback to be invoked when shipping rates are retrieved.
|
||||
onShippingRateSuccess: ReturnType< typeof emitterCallback >;
|
||||
// The current shipping error status.
|
||||
shippingErrorStatus: ShippingErrorStatus;
|
||||
// The error type constants for the shipping rate error status.
|
||||
shippingErrorTypes: ShippingErrorTypes;
|
||||
};
|
||||
|
||||
export interface ShippingDataProviderProps {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
}
|
|
@ -15,28 +15,24 @@ export const DEFAULT_ERROR_MESSAGE = __(
|
|||
'woo-gutenberg-products-block'
|
||||
);
|
||||
|
||||
export const hasStoreNoticesContainer = ( container: string ): boolean => {
|
||||
const containers = select( 'wc/store/store-notices' ).getContainers();
|
||||
return containers.includes( container );
|
||||
/**
|
||||
* Returns a list of all notice contexts defined by Blocks.
|
||||
*
|
||||
* Contexts are defined in enum format, but this returns an array of strings instead.
|
||||
*/
|
||||
export const getNoticeContexts = () => {
|
||||
return Object.values( noticeContexts );
|
||||
};
|
||||
|
||||
const findParentContainer = ( container: string ): string => {
|
||||
if ( container.includes( noticeContexts.CHECKOUT + '/' ) ) {
|
||||
return noticeContexts.CHECKOUT;
|
||||
}
|
||||
if ( container.includes( noticeContexts.CART + '/' ) ) {
|
||||
return hasStoreNoticesContainer( noticeContexts.CART )
|
||||
? noticeContexts.CART
|
||||
: noticeContexts.CHECKOUT;
|
||||
}
|
||||
return container;
|
||||
const hasStoreNoticesContainer = ( container: string ): boolean => {
|
||||
const containers = select(
|
||||
'wc/store/store-notices'
|
||||
).getRegisteredContainers();
|
||||
return containers.includes( container );
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper for @wordpress/notices createNotice.
|
||||
*
|
||||
* This is used to create the correct type of notice based on the provided context, and to ensure the notice container
|
||||
* exists first, otherwise it uses the default context instead.
|
||||
*/
|
||||
export const createNotice = (
|
||||
status: 'error' | 'warning' | 'info' | 'success',
|
||||
|
@ -51,14 +47,10 @@ export const createNotice = (
|
|||
return;
|
||||
}
|
||||
|
||||
const { createNotice: dispatchCreateNotice } = dispatch( 'core/notices' );
|
||||
|
||||
dispatchCreateNotice( status, message, {
|
||||
dispatch( 'core/notices' ).createNotice( status, message, {
|
||||
isDismissible: true,
|
||||
...options,
|
||||
context: hasStoreNoticesContainer( noticeContext )
|
||||
? noticeContext
|
||||
: findParentContainer( noticeContext ),
|
||||
context: noticeContext,
|
||||
} );
|
||||
};
|
||||
|
||||
|
@ -82,7 +74,9 @@ export const createNoticeIfVisible = (
|
|||
* @see https://github.com/WordPress/gutenberg/pull/44059
|
||||
*/
|
||||
export const removeAllNotices = () => {
|
||||
const containers = select( 'wc/store/store-notices' ).getContainers();
|
||||
const containers = select(
|
||||
'wc/store/store-notices'
|
||||
).getRegisteredContainers();
|
||||
const { removeNotice } = dispatch( 'core/notices' );
|
||||
const { getNotices } = select( 'core/notices' );
|
||||
|
||||
|
|
|
@ -4,9 +4,6 @@
|
|||
import classnames from 'classnames';
|
||||
import { SidebarLayout } from '@woocommerce/base-components/sidebar-layout';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -20,44 +17,8 @@ const FrontendBlock = ( {
|
|||
children: JSX.Element | JSX.Element[];
|
||||
className: string;
|
||||
} ): JSX.Element | null => {
|
||||
const { cartItems, cartIsLoading, cartItemErrors } = useStoreCart();
|
||||
const { cartItems, cartIsLoading } = useStoreCart();
|
||||
const { hasDarkControls } = useCartBlockContext();
|
||||
const { createErrorNotice, removeNotice } = useDispatch( 'core/notices' );
|
||||
|
||||
/*
|
||||
* The code for removing old notices is also present in the filled-mini-cart-contents-block/frontend.tsx file and
|
||||
* will take care of removing outdated errors in the Mini Cart block.
|
||||
*/
|
||||
const currentlyDisplayedErrorNoticeCodes = useSelect( ( select ) => {
|
||||
return select( 'core/notices' )
|
||||
.getNotices( 'wc/cart' )
|
||||
.filter(
|
||||
( notice ) =>
|
||||
notice.status === 'error' && notice.type === 'default'
|
||||
)
|
||||
.map( ( notice ) => notice.id );
|
||||
} );
|
||||
|
||||
useEffect( () => {
|
||||
// Clear errors out of the store before adding the new ones from the response.
|
||||
currentlyDisplayedErrorNoticeCodes.forEach( ( id ) => {
|
||||
removeNotice( id, 'wc/cart' );
|
||||
} );
|
||||
|
||||
// Ensures any cart errors listed in the API response get shown.
|
||||
cartItemErrors.forEach( ( error ) => {
|
||||
createErrorNotice( decodeEntities( error.message ), {
|
||||
isDismissible: true,
|
||||
id: error.code,
|
||||
context: 'wc/cart',
|
||||
} );
|
||||
} );
|
||||
}, [
|
||||
createErrorNotice,
|
||||
cartItemErrors,
|
||||
currentlyDisplayedErrorNoticeCodes,
|
||||
removeNotice,
|
||||
] );
|
||||
|
||||
if ( cartIsLoading || cartItems.length >= 1 ) {
|
||||
return (
|
||||
|
|
|
@ -184,6 +184,7 @@ const Block = ( {
|
|||
showErrorMessage={ CURRENT_USER_IS_ADMIN }
|
||||
>
|
||||
<StoreNoticesContainer context={ noticeContexts.CHECKOUT } />
|
||||
<StoreNoticesContainer context={ noticeContexts.CART } />
|
||||
{ /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ }
|
||||
<SlotFillProvider>
|
||||
<CheckoutProvider>
|
||||
|
|
|
@ -4,10 +4,6 @@
|
|||
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
|
||||
type FilledMiniCartContentsBlockProps = {
|
||||
children: JSX.Element;
|
||||
className: string;
|
||||
|
@ -17,44 +13,7 @@ const FilledMiniCartContentsBlock = ( {
|
|||
children,
|
||||
className,
|
||||
}: FilledMiniCartContentsBlockProps ): JSX.Element | null => {
|
||||
const { cartItems, cartItemErrors } = useStoreCart();
|
||||
|
||||
const { createErrorNotice, removeNotice } = useDispatch( 'core/notices' );
|
||||
|
||||
/*
|
||||
* The code for removing old notices is also present in the filled-cart-block/frontend.tsx file and will take care
|
||||
* of removing outdated errors in the Cart block.
|
||||
*/
|
||||
const currentlyDisplayedErrorNoticeCodes = useSelect( ( select ) => {
|
||||
return select( 'core/notices' )
|
||||
.getNotices( 'wc/cart' )
|
||||
.filter(
|
||||
( notice ) =>
|
||||
notice.status === 'error' && notice.type === 'default'
|
||||
)
|
||||
.map( ( notice ) => notice.id );
|
||||
} );
|
||||
|
||||
// Ensures any cart errors listed in the API response get shown.
|
||||
useEffect( () => {
|
||||
// Clear errors out of the store before adding the new ones from the response.
|
||||
currentlyDisplayedErrorNoticeCodes.forEach( ( id ) => {
|
||||
removeNotice( id, 'wc/cart' );
|
||||
} );
|
||||
|
||||
cartItemErrors.forEach( ( error ) => {
|
||||
createErrorNotice( decodeEntities( error.message ), {
|
||||
isDismissible: false,
|
||||
id: error.code,
|
||||
context: 'wc/cart',
|
||||
} );
|
||||
} );
|
||||
}, [
|
||||
createErrorNotice,
|
||||
cartItemErrors,
|
||||
currentlyDisplayedErrorNoticeCodes,
|
||||
removeNotice,
|
||||
] );
|
||||
const { cartItems } = useStoreCart();
|
||||
|
||||
if ( cartItems.length === 0 ) {
|
||||
return null;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
export const ACTION_TYPES = {
|
||||
SET_CART_DATA: 'SET_CART_DATA',
|
||||
RECEIVE_ERROR: 'RECEIVE_ERROR',
|
||||
REPLACE_ERRORS: 'REPLACE_ERRORS',
|
||||
SET_ERROR_DATA: 'SET_ERROR_DATA',
|
||||
APPLYING_COUPON: 'APPLYING_COUPON',
|
||||
REMOVING_COUPON: 'REMOVING_COUPON',
|
||||
RECEIVE_CART_ITEM: 'RECEIVE_CART_ITEM',
|
||||
|
|
|
@ -39,6 +39,20 @@ export const setCartData = ( cart: Cart ): { type: string; response: Cart } => {
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* An action creator that dispatches the plain action responsible for setting the cart error data in the store.
|
||||
*
|
||||
* @param error the parsed error object (Parsed into camelCase).
|
||||
*/
|
||||
export const setErrorData = (
|
||||
error: ApiErrorResponse | null
|
||||
): { type: string; response: ApiErrorResponse | null } => {
|
||||
return {
|
||||
type: types.SET_ERROR_DATA,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an action object used in updating the store with the provided cart.
|
||||
*
|
||||
|
@ -62,18 +76,6 @@ export const receiveCartContents = (
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an action object used for receiving customer facing errors from the API.
|
||||
*/
|
||||
export const receiveError = (
|
||||
error: ApiErrorResponse | null = null,
|
||||
replace = true
|
||||
) =>
|
||||
( {
|
||||
type: replace ? types.REPLACE_ERRORS : types.RECEIVE_ERROR,
|
||||
error,
|
||||
} as const );
|
||||
|
||||
/**
|
||||
* Returns an action object used to track when a coupon is applying.
|
||||
*
|
||||
|
@ -195,13 +197,6 @@ export const applyExtensionCartUpdate =
|
|||
return response;
|
||||
} catch ( error ) {
|
||||
dispatch.receiveError( error );
|
||||
// If updated cart state was returned, also update that.
|
||||
if ( error.data?.cart ) {
|
||||
dispatch.receiveCart( error.data.cart );
|
||||
}
|
||||
|
||||
// Re-throw the error.
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -230,14 +225,6 @@ export const applyCoupon =
|
|||
dispatch.receiveCart( response );
|
||||
} catch ( error ) {
|
||||
dispatch.receiveError( error );
|
||||
|
||||
// If updated cart state was returned, also update that.
|
||||
if ( error.data?.cart ) {
|
||||
dispatch.receiveCart( error.data.cart );
|
||||
}
|
||||
|
||||
// Re-throw the error.
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -268,14 +255,6 @@ export const removeCoupon =
|
|||
dispatch.receiveCart( response );
|
||||
} catch ( error ) {
|
||||
dispatch.receiveError( error );
|
||||
|
||||
// If updated cart state was returned, also update that.
|
||||
if ( error.data?.cart ) {
|
||||
dispatch.receiveCart( error.data.cart );
|
||||
}
|
||||
|
||||
// Re-throw the error.
|
||||
throw error;
|
||||
} finally {
|
||||
dispatch.receiveRemovingCoupon( '' );
|
||||
}
|
||||
|
@ -312,14 +291,6 @@ export const addItemToCart =
|
|||
triggerAddedToCartEvent( { preserveCartData: true } );
|
||||
} catch ( error ) {
|
||||
dispatch.receiveError( error );
|
||||
|
||||
// If updated cart state was returned, also update that.
|
||||
if ( error.data?.cart ) {
|
||||
dispatch.receiveCart( error.data.cart );
|
||||
}
|
||||
|
||||
// Re-throw the error.
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -350,11 +321,6 @@ export const removeItemFromCart =
|
|||
dispatch.receiveCart( response );
|
||||
} catch ( error ) {
|
||||
dispatch.receiveError( error );
|
||||
|
||||
// If updated cart state was returned, also update that.
|
||||
if ( error.data?.cart ) {
|
||||
dispatch.receiveCart( error.data.cart );
|
||||
}
|
||||
} finally {
|
||||
dispatch.itemIsPendingDelete( cartItemKey, false );
|
||||
}
|
||||
|
@ -401,11 +367,6 @@ export const changeCartItemQuantity =
|
|||
dispatch.receiveCart( response );
|
||||
} catch ( error ) {
|
||||
dispatch.receiveError( error );
|
||||
|
||||
// If updated cart state was returned, also update that.
|
||||
if ( error.data?.cart ) {
|
||||
dispatch.receiveCart( error.data.cart );
|
||||
}
|
||||
} finally {
|
||||
dispatch.itemIsPendingQuantity( cartItemKey, false );
|
||||
}
|
||||
|
@ -436,14 +397,6 @@ export const selectShippingRate =
|
|||
dispatch.receiveCart( response );
|
||||
} catch ( error ) {
|
||||
dispatch.receiveError( error );
|
||||
|
||||
// If updated cart state was returned, also update that.
|
||||
if ( error.data?.cart ) {
|
||||
dispatch.receiveCart( error.data.cart );
|
||||
}
|
||||
|
||||
// Re-throw the error.
|
||||
throw error;
|
||||
} finally {
|
||||
dispatch.shippingRatesBeingSelected( false );
|
||||
}
|
||||
|
@ -494,11 +447,6 @@ export const updateCustomerData =
|
|||
dispatch.receiveError( error );
|
||||
dispatch.updatingCustomerData( false );
|
||||
|
||||
// If updated cart state was returned, also update that.
|
||||
if ( error.data?.cart ) {
|
||||
dispatch.receiveCart( error.data.cart );
|
||||
}
|
||||
|
||||
return Promise.reject( error );
|
||||
}
|
||||
return Promise.resolve( true );
|
||||
|
@ -508,7 +456,7 @@ export type CartAction = ReturnOrGeneratorYieldUnion<
|
|||
| typeof receiveCartContents
|
||||
| typeof setBillingAddress
|
||||
| typeof setShippingAddress
|
||||
| typeof receiveError
|
||||
| typeof setErrorData
|
||||
| typeof receiveApplyingCoupon
|
||||
| typeof receiveRemovingCoupon
|
||||
| typeof receiveCartItem
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { ApiErrorResponse, isApiErrorResponse } from '@woocommerce/types';
|
||||
import { createNotice, DEFAULT_ERROR_MESSAGE } from '@woocommerce/base-utils';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import { dispatch } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* This function is used to notify the user of cart errors.
|
||||
*/
|
||||
export const notifyErrors = ( error: ApiErrorResponse | null = null ) => {
|
||||
if ( error === null || ! isApiErrorResponse( error ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
let errorMessage = error.message || DEFAULT_ERROR_MESSAGE;
|
||||
|
||||
// Replace the generic invalid JSON message with something more user friendly.
|
||||
if ( error.code === 'invalid_json' ) {
|
||||
errorMessage = __(
|
||||
'Something went wrong. Please contact us for assistance.',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
}
|
||||
|
||||
// Create a new notice with a consistent error ID.
|
||||
createNotice( 'error', errorMessage, {
|
||||
id: 'woocommerce_cart_data_request_error',
|
||||
context: 'wc/cart',
|
||||
isDismissible: true,
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* This function is used to notify the user of cart item errors/conflicts
|
||||
*/
|
||||
export const notifyCartErrors = (
|
||||
errors: ApiErrorResponse[] | null = null,
|
||||
oldErrors: ApiErrorResponse[] | null = null
|
||||
) => {
|
||||
if ( oldErrors ) {
|
||||
oldErrors.forEach( ( error ) => {
|
||||
dispatch( 'core/notices' ).removeNotice( error.code, 'wc/cart' );
|
||||
} );
|
||||
}
|
||||
|
||||
if ( errors !== null ) {
|
||||
errors.forEach( ( error ) => {
|
||||
if ( isApiErrorResponse( error ) ) {
|
||||
createNotice( 'error', decodeEntities( error.message ), {
|
||||
id: error.code,
|
||||
context: 'wc/cart',
|
||||
isDismissible: true,
|
||||
} );
|
||||
}
|
||||
} );
|
||||
}
|
||||
};
|
|
@ -48,15 +48,7 @@ const reducer: Reducer< CartState > = (
|
|||
action: Partial< CartAction >
|
||||
) => {
|
||||
switch ( action.type ) {
|
||||
case types.RECEIVE_ERROR:
|
||||
if ( action.error ) {
|
||||
state = {
|
||||
...state,
|
||||
errors: state.errors.concat( action.error ),
|
||||
};
|
||||
}
|
||||
break;
|
||||
case types.REPLACE_ERRORS:
|
||||
case types.SET_ERROR_DATA:
|
||||
if ( action.error ) {
|
||||
state = {
|
||||
...state,
|
||||
|
|
|
@ -54,9 +54,9 @@ describe( 'cartReducer', () => {
|
|||
totals: {},
|
||||
} );
|
||||
} );
|
||||
it( 'sets expected state when errors are replaced', () => {
|
||||
it( 'sets expected state when errors are set', () => {
|
||||
const testAction = {
|
||||
type: types.REPLACE_ERRORS,
|
||||
type: types.SET_ERROR_DATA,
|
||||
error: {
|
||||
code: '101',
|
||||
message: 'Test Error',
|
||||
|
@ -73,30 +73,6 @@ describe( 'cartReducer', () => {
|
|||
},
|
||||
] );
|
||||
} );
|
||||
it( 'sets expected state when an error is added', () => {
|
||||
const testAction = {
|
||||
type: types.RECEIVE_ERROR,
|
||||
error: {
|
||||
code: '101',
|
||||
message: 'Test Error',
|
||||
data: {},
|
||||
},
|
||||
};
|
||||
const newState = cartReducer( originalState, testAction );
|
||||
expect( newState ).not.toBe( originalState );
|
||||
expect( newState.errors ).toEqual( [
|
||||
{
|
||||
code: '100',
|
||||
message: 'Test Error',
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
code: '101',
|
||||
message: 'Test Error',
|
||||
data: {},
|
||||
},
|
||||
] );
|
||||
} );
|
||||
it( 'sets expected state when a coupon is applied', () => {
|
||||
const testAction = {
|
||||
type: types.APPLYING_COUPON,
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { CartResponse, Cart } from '@woocommerce/types';
|
||||
import {
|
||||
CartResponse,
|
||||
Cart,
|
||||
ApiErrorResponse,
|
||||
isApiErrorResponse,
|
||||
} from '@woocommerce/types';
|
||||
import { camelCase, mapKeys } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { notifyQuantityChanges } from './notify-quantity-changes';
|
||||
import { notifyErrors, notifyCartErrors } from './notify-errors';
|
||||
import { CartDispatchFromMap, CartSelectFromMap } from './index';
|
||||
|
||||
/**
|
||||
|
@ -25,14 +31,33 @@ export const receiveCart =
|
|||
dispatch: CartDispatchFromMap;
|
||||
select: CartSelectFromMap;
|
||||
} ) => {
|
||||
const cart = mapKeys( response, ( _, key ) =>
|
||||
const newCart = mapKeys( response, ( _, key ) =>
|
||||
camelCase( key )
|
||||
) as unknown as Cart;
|
||||
const oldCart = select.getCartData();
|
||||
notifyCartErrors( newCart.errors, oldCart.errors );
|
||||
notifyQuantityChanges( {
|
||||
oldCart: select.getCartData(),
|
||||
newCart: cart,
|
||||
oldCart,
|
||||
newCart,
|
||||
cartItemsPendingQuantity: select.getItemsPendingQuantityUpdate(),
|
||||
cartItemsPendingDelete: select.getItemsPendingDelete(),
|
||||
} );
|
||||
dispatch.setCartData( cart );
|
||||
dispatch.setCartData( newCart );
|
||||
};
|
||||
|
||||
/**
|
||||
* A thunk used in updating the store with cart errors retrieved from a request. This also notifies the shopper of any errors that occur.
|
||||
*/
|
||||
export const receiveError =
|
||||
( response: ApiErrorResponse | null = null ) =>
|
||||
( { dispatch }: { dispatch: CartDispatchFromMap } ) => {
|
||||
if ( isApiErrorResponse( response ) ) {
|
||||
dispatch.setErrorData( response );
|
||||
|
||||
if ( response.data?.cart ) {
|
||||
dispatch.receiveCart( response?.data?.cart );
|
||||
}
|
||||
|
||||
notifyErrors( response );
|
||||
}
|
||||
};
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
*/
|
||||
import { StoreNoticesState } from './default-state';
|
||||
|
||||
export const getContainers = (
|
||||
export const getRegisteredContainers = (
|
||||
state: StoreNoticesState
|
||||
): StoreNoticesState[ 'containers' ] => state.containers;
|
||||
|
|
|
@ -7,7 +7,11 @@ import {
|
|||
DEFAULT_ERROR_MESSAGE,
|
||||
} from '@woocommerce/base-utils';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import { isObject, objectHasProp, ApiErrorResponse } from '@woocommerce/types';
|
||||
import {
|
||||
objectHasProp,
|
||||
ApiErrorResponse,
|
||||
isApiErrorResponse,
|
||||
} from '@woocommerce/types';
|
||||
import { noticeContexts } from '@woocommerce/base-context/event-emit/utils';
|
||||
|
||||
type ApiParamError = {
|
||||
|
@ -17,15 +21,6 @@ type ApiParamError = {
|
|||
message: string;
|
||||
};
|
||||
|
||||
const isApiResponse = ( response: unknown ): response is ApiErrorResponse => {
|
||||
return (
|
||||
isObject( response ) &&
|
||||
objectHasProp( response, 'code' ) &&
|
||||
objectHasProp( response, 'message' ) &&
|
||||
objectHasProp( response, 'data' )
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Flattens error details which are returned from the API when multiple params are not valid.
|
||||
*
|
||||
|
@ -131,7 +126,7 @@ export const processErrorResponse = (
|
|||
response: ApiErrorResponse,
|
||||
context: string | undefined
|
||||
) => {
|
||||
if ( ! isApiResponse( response ) ) {
|
||||
if ( ! isApiErrorResponse( response ) ) {
|
||||
return;
|
||||
}
|
||||
switch ( response.code ) {
|
||||
|
|
|
@ -7,7 +7,7 @@ import type { CartResponse } from './cart-response';
|
|||
export type ApiErrorResponse = {
|
||||
code: string;
|
||||
message: string;
|
||||
data: ApiErrorResponseData;
|
||||
data?: ApiErrorResponseData | undefined;
|
||||
};
|
||||
|
||||
// API errors contain data with the status, and more in-depth error details. This may be null.
|
||||
|
|
|
@ -11,16 +11,6 @@
|
|||
* included (in subunits).
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CartShippingOption
|
||||
*
|
||||
* @property {string} name Name of the shipping rate
|
||||
* @property {string} description Description of the shipping rate.
|
||||
* @property {string} price Price of the shipping rate (in subunits)
|
||||
* @property {string} rate_id The ID of the shipping rate.
|
||||
* @property {string} delivery_time The delivery time of the shipping rate
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CartItemImage
|
||||
*
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
/* eslint-disable jsdoc/valid-types */
|
||||
/**
|
||||
* @typedef {import('./billing').BillingData} BillingData
|
||||
* @typedef {import('./cart').CartShippingOption} CartShippingOption
|
||||
* @typedef {import('./shipping').ShippingAddress} CartShippingAddress
|
||||
* @typedef {import('./cart').CartData} CartData
|
||||
* @typedef {import('./checkout').CheckoutDispatchActions} CheckoutDispatchActions
|
||||
|
@ -10,32 +9,6 @@
|
|||
* @typedef {import('./add-to-cart-form').AddToCartFormEventRegistration} AddToCartFormEventRegistration
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ShippingDataContext
|
||||
*
|
||||
* @property {ShippingErrorStatus} shippingErrorStatus The current shipping error status.
|
||||
* @property {Function} dispatchErrorStatus A function for dispatching a shipping rate error status.
|
||||
* @property {ShippingErrorTypes} shippingErrorTypes The error type constants for the shipping rate error
|
||||
* status.
|
||||
* @property {CartShippingOption[]} shippingRates An array of available shipping rates.
|
||||
* @property {boolean} shippingRatesLoading Whether or not the shipping rates are being loaded.
|
||||
* @property {string[]} selectedRates The ids of the rates that are selected.
|
||||
* @property {function()} setSelectedRates Function for setting the selected rates.
|
||||
* @property {boolean} isSelectingRate True when rate is being selected.
|
||||
* @property {CartShippingAddress} shippingAddress The current set address for shipping.
|
||||
* @property {function(Object)} setShippingAddress Function for setting the shipping address.
|
||||
* @property {function()} onShippingRateSuccess Used to register a callback to be invoked when shipping
|
||||
* rates are retrieved.
|
||||
* @property {function()} onShippingRateSelectSuccess Used to register a callback to be invoked when shipping
|
||||
* rate is selected.
|
||||
* @property {function()} onShippingRateSelectFail Used to register a callback to be invoked when shipping
|
||||
* rate is selected unsuccessfully
|
||||
* @property {function()} onShippingRateFail Used to register a callback to be invoked when there is
|
||||
* an error with retrieving shipping rates.
|
||||
* @property {boolean} needsShipping True if the cart has items requiring shipping.
|
||||
* @property {boolean} hasCalculatedShipping True if the cart has calculated shipping costs.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ShippingErrorStatus
|
||||
*
|
||||
|
|
|
@ -11,13 +11,3 @@ export interface PackageRateOption {
|
|||
secondaryDescription?: string | ReactElement | undefined;
|
||||
id?: string | undefined;
|
||||
}
|
||||
|
||||
export interface SelectShippingRateType {
|
||||
// Returns a function that accepts a shipping rate ID and a package ID.
|
||||
selectShippingRate: (
|
||||
newShippingRateId: string,
|
||||
packageId?: string | number
|
||||
) => unknown;
|
||||
// True when a rate is currently being selected and persisted to the server.
|
||||
isSelectingRate: boolean;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { isObject, objectHasProp } from './object';
|
||||
import type { ApiErrorResponse } from '../type-defs';
|
||||
|
||||
// Type guard for ApiErrorResponse.
|
||||
export const isApiErrorResponse = (
|
||||
response: unknown
|
||||
): response is ApiErrorResponse => {
|
||||
return (
|
||||
isObject( response ) &&
|
||||
objectHasProp( response, 'code' ) &&
|
||||
objectHasProp( response, 'message' )
|
||||
);
|
||||
};
|
|
@ -9,3 +9,4 @@ export * from './string';
|
|||
export * from './attributes';
|
||||
export * from './ratings';
|
||||
export * from './stock-status';
|
||||
export * from './api-error-response';
|
||||
|
|
|
@ -2,7 +2,12 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { PAYMENT_STORE_KEY } from '@woocommerce/block-data';
|
||||
import {
|
||||
PAYMENT_STORE_KEY,
|
||||
STORE_NOTICES_STORE_KEY,
|
||||
} from '@woocommerce/block-data';
|
||||
import { getNoticeContexts } from '@woocommerce/base-utils';
|
||||
import type { Notice } from '@wordpress/notices';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -12,14 +17,11 @@ import StoreNotices from './store-notices';
|
|||
import SnackbarNotices from './snackbar-notices';
|
||||
import type { StoreNoticesContainerProps, StoreNotice } from './types';
|
||||
|
||||
const formatNotices = (
|
||||
notices: StoreNotice[],
|
||||
context: string
|
||||
): StoreNotice[] => {
|
||||
const formatNotices = ( notices: Notice[], context: string ): StoreNotice[] => {
|
||||
return notices.map( ( notice ) => ( {
|
||||
...notice,
|
||||
context,
|
||||
} ) );
|
||||
} ) ) as StoreNotice[];
|
||||
};
|
||||
|
||||
const StoreNoticesContainer = ( {
|
||||
|
@ -27,22 +29,41 @@ const StoreNoticesContainer = ( {
|
|||
context,
|
||||
additionalNotices = [],
|
||||
}: StoreNoticesContainerProps ): JSX.Element | null => {
|
||||
const suppressNotices = useSelect( ( select ) =>
|
||||
select( PAYMENT_STORE_KEY ).isExpressPaymentMethodActive()
|
||||
const { suppressNotices, registeredContainers } = useSelect(
|
||||
( select ) => ( {
|
||||
suppressNotices:
|
||||
select( PAYMENT_STORE_KEY ).isExpressPaymentMethodActive(),
|
||||
registeredContainers: select(
|
||||
STORE_NOTICES_STORE_KEY
|
||||
).getRegisteredContainers(),
|
||||
} )
|
||||
);
|
||||
|
||||
// Find sub-contexts that have not been registered. We will show notices from those contexts here too.
|
||||
const allContexts = getNoticeContexts();
|
||||
const unregisteredSubContexts = allContexts.filter(
|
||||
( subContext: string ) =>
|
||||
subContext.includes( context + '/' ) &&
|
||||
! registeredContainers.includes( subContext )
|
||||
);
|
||||
|
||||
// Get notices from the current context and any sub-contexts and append the name of the context to the notice
|
||||
// objects for later reference.
|
||||
const notices = useSelect< StoreNotice[] >( ( select ) => {
|
||||
const { getNotices } = select( 'core/notices' );
|
||||
|
||||
return formatNotices(
|
||||
( getNotices( context ) as StoreNotice[] ).concat(
|
||||
additionalNotices
|
||||
return [
|
||||
...unregisteredSubContexts.flatMap( ( subContext: string ) =>
|
||||
formatNotices( getNotices( subContext ), subContext )
|
||||
),
|
||||
context
|
||||
).filter( Boolean ) as StoreNotice[];
|
||||
...formatNotices(
|
||||
getNotices( context ).concat( additionalNotices ),
|
||||
context
|
||||
),
|
||||
].filter( Boolean ) as StoreNotice[];
|
||||
} );
|
||||
|
||||
if ( suppressNotices ) {
|
||||
if ( suppressNotices || ! notices.length ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,8 @@ const StoreNotices = ( {
|
|||
|
||||
if (
|
||||
activeElement &&
|
||||
inputs.indexOf( activeElement.tagName.toLowerCase() ) !== -1
|
||||
inputs.indexOf( activeElement.tagName.toLowerCase() ) !== -1 &&
|
||||
activeElement.getAttribute( 'type' ) !== 'radio'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
@ -72,7 +73,7 @@ const StoreNotices = ( {
|
|||
};
|
||||
}, [ context, registerContainer, unregisterContainer ] );
|
||||
|
||||
// Group notices by whether or not they are dismissable. Dismissable notices can be grouped.
|
||||
// Group notices by whether or not they are dismissible. Dismissible notices can be grouped.
|
||||
const dismissibleNotices = notices.filter(
|
||||
( { isDismissible } ) => !! isDismissible
|
||||
);
|
||||
|
@ -101,7 +102,7 @@ const StoreNotices = ( {
|
|||
>
|
||||
{ nonDismissibleNotices.map( ( notice ) => (
|
||||
<Notice
|
||||
key={ notice.id }
|
||||
key={ notice.id + '-' + notice.context }
|
||||
className={ classnames(
|
||||
'wc-block-components-notices__notice',
|
||||
getClassNameFromStatus( notice.status )
|
||||
|
@ -140,7 +141,11 @@ const StoreNotices = ( {
|
|||
) : (
|
||||
<ul>
|
||||
{ noticeGroup.map( ( notice ) => (
|
||||
<li key={ notice.id }>
|
||||
<li
|
||||
key={
|
||||
notice.id + '-' + notice.context
|
||||
}
|
||||
>
|
||||
{ sanitizeHTML(
|
||||
decodeEntities( notice.content )
|
||||
) }
|
||||
|
|
|
@ -97,8 +97,44 @@ describe( 'StoreNoticesContainer', () => {
|
|||
] }
|
||||
/>
|
||||
);
|
||||
// Also counts the spokenMessage.
|
||||
expect( screen.getAllByText( /Additional test error/i ) ).toHaveLength(
|
||||
2
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'Shows notices from unregistered sub-contexts', async () => {
|
||||
dispatch( noticesStore ).createErrorNotice(
|
||||
'Custom sub-context error',
|
||||
{
|
||||
id: 'custom-subcontext-test-error',
|
||||
context: 'wc/checkout/shipping-address',
|
||||
}
|
||||
);
|
||||
dispatch( noticesStore ).createErrorNotice(
|
||||
'Custom sub-context error',
|
||||
{
|
||||
id: 'custom-subcontext-test-error',
|
||||
context: 'wc/checkout/billing-address',
|
||||
}
|
||||
);
|
||||
render( <StoreNoticesContainer context="wc/checkout" /> );
|
||||
// This should match against 3 elements; 2 error messages, and the spoken message where they are combined into one element.
|
||||
expect(
|
||||
screen.getAllByText( /Custom sub-context error/i )
|
||||
).toHaveLength( 3 );
|
||||
// Clean up notices.
|
||||
await act( () =>
|
||||
dispatch( noticesStore ).removeNotice(
|
||||
'custom-subcontext-test-error',
|
||||
'wc/checkout/shipping-address'
|
||||
)
|
||||
);
|
||||
await act( () =>
|
||||
dispatch( noticesStore ).removeNotice(
|
||||
'custom-subcontext-test-error',
|
||||
'wc/checkout/billing-address'
|
||||
)
|
||||
);
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -36,5 +36,4 @@ export const useShippingData = () => ( {
|
|||
hasCalculatedShipping: previewCart.has_calculated_shipping,
|
||||
isLoadingRates: false,
|
||||
isCollectable: false,
|
||||
hasSelectedLocalPickup: false,
|
||||
} );
|
||||
|
|
Loading…
Reference in New Issue