Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Commit

Permalink
Capture notices from hidden block into siblings block (#8390)
Browse files Browse the repository at this point in the history
* Capture notices from hidden block into siblings block

* switch to using a single context

* make change bwc

* add tests

* support context as array in StoreNotice

* move filter logic to Notice component
  • Loading branch information
senadir authored Feb 9, 2023
1 parent d5d401c commit 95702ed
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,9 @@ const ShippingCalculator = ( {
addressFields = [ 'country', 'state', 'city', 'postcode' ],
}: ShippingCalculatorProps ): JSX.Element => {
const { shippingAddress } = useCustomerData();
const noticeContext = 'wc/cart/shipping-calculator';
return (
<div className="wc-block-components-shipping-calculator">
<StoreNoticesContainer context={ noticeContext } />
<StoreNoticesContainer context={ 'wc/cart/shipping-calculator' } />
<ShippingCalculatorAddress
address={ shippingAddress }
addressFields={ addressFields }
Expand Down
5 changes: 3 additions & 2 deletions assets/js/blocks/checkout/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,9 @@ const Block = ( {
) }
showErrorMessage={ CURRENT_USER_IS_ADMIN }
>
<StoreNoticesContainer context={ noticeContexts.CHECKOUT } />
<StoreNoticesContainer context={ noticeContexts.CART } />
<StoreNoticesContainer
context={ [ noticeContexts.CHECKOUT, noticeContexts.CART ] }
/>
{ /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ }
<SlotFillProvider>
<CheckoutProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,13 @@ const Block = ( {
] ) as Record< keyof AddressFields, Partial< AddressField > >;

const AddressFormWrapperComponent = isEditor ? Noninteractive : Fragment;
const noticeContext = useBillingAsShipping
? [ noticeContexts.BILLING_ADDRESS, noticeContexts.SHIPPING_ADDRESS ]
: [ noticeContexts.BILLING_ADDRESS ];

return (
<AddressFormWrapperComponent>
<StoreNoticesContainer context={ noticeContexts.BILLING_ADDRESS } />
<StoreNoticesContainer context={ noticeContext } />
<AddressForm
id="billing"
type="billing"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,14 @@ const Block = ( {
] ) as Record< keyof AddressFields, Partial< AddressField > >;

const AddressFormWrapperComponent = isEditor ? Noninteractive : Fragment;
const noticeContext = useShippingAsBilling
? [ noticeContexts.SHIPPING_ADDRESS, noticeContexts.BILLING_ADDRESS ]
: [ noticeContexts.SHIPPING_ADDRESS ];

return (
<>
<AddressFormWrapperComponent>
<StoreNoticesContainer
context={ noticeContexts.SHIPPING_ADDRESS }
/>
<StoreNoticesContainer context={ noticeContext } />
<AddressForm
id="shipping"
type="shipping"
Expand Down
36 changes: 21 additions & 15 deletions docs/internal-developers/block-client-apis/notices.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
- [Notices in WooCommerce Blocks](#notices-in-woocommerce-blocks)
- [`StoreNoticesContainer`](#storenoticescontainer)
- [Snackbar notices in WooCommerce Blocks](#snackbar-notices-in-woocommerce-blocks)
- [`SnackbarNoticesContainer`](#snackbarnoticescontainer)

## Notices in WooCommerce Blocks

Expand All @@ -19,6 +18,10 @@ The below example will show all notices with type `default` that are in the `wc/

On the Cart Block, a `StoreNoticesContainer` is already rendered with the `wc/cart` context, and on the Checkout Block, a `StoreNoticesContainer` is already rendered with the `wc/checkout` context. To display errors from other contexts, you can use the `StoreNoticesContainer` component with context passed as a prop.

`StoreNoticesContainer` also support passing an array of context strings to it, this allows you to capture several contexts at once, while filtering out similar notices.

#### Single context

```jsx
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';

Expand All @@ -27,6 +30,23 @@ const PaymentErrors = () => {
};
```

#### Multiple contexts

```jsx
import { StoreNoticesContainer } from '@woocommerce/blocks-checkout';

const AddressForm = () => {
return (
<StoreNoticesContainer
context={ [
'wc/checkout/shipping-address',
'wc/checkout/billing-address',
] }
/>
);
};
```

## Snackbar notices in WooCommerce Blocks

WooCommerce Blocks also shows snackbar notices, to add a snackbar notice you need to create a notice with `type:snackbar` in the options object.
Expand All @@ -53,17 +73,3 @@ dispatch( 'core/notices' ).createNotice(
'snackbar-notice-id'
);
```

### `SnackbarNoticesContainer`

To display snackbar notices, use the `SnackbarNoticesContainer` component. This component is rendered with the Cart and Checkout blocks, so there is no need to add another. The context it displays notices for is `default`. If, for some reason you do need to show snackbar messages for a different context, you can render this component again and pass the context as a prop to the component.

```jsx
import { SnackbarNoticesContainer } from '@woocommerce/base-components/snackbar-notices-container';

const AlternativeSnackbarNotices = () => {
return (
<SnackbarNoticesContainer context="wc/alternative-snackbar-notices" />
);
};
```
18 changes: 10 additions & 8 deletions packages/checkout/components/store-notices-container/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@ const StoreNoticesContainer = ( {
).getRegisteredContainers(),
} )
);

const contexts = Array.isArray( context ) ? context : [ context ];
// 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 )
contexts.some( ( _context: 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
Expand All @@ -56,13 +57,14 @@ const StoreNoticesContainer = ( {
...unregisteredSubContexts.flatMap( ( subContext: string ) =>
formatNotices( getNotices( subContext ), subContext )
),
...formatNotices(
getNotices( context ).concat( additionalNotices ),
context
...contexts.flatMap( ( subContext: string ) =>
formatNotices(
getNotices( subContext ).concat( additionalNotices ),
subContext
)
),
].filter( Boolean ) as StoreNotice[];
} );

if ( suppressNotices || ! notices.length ) {
return null;
}
Expand All @@ -71,7 +73,7 @@ const StoreNoticesContainer = ( {
<>
<StoreNotices
className={ className }
context={ context }
context={ contexts }
notices={ notices.filter(
( notice ) => notice.type === 'default'
) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const StoreNotices = ( {
className,
notices,
}: {
context: string;
context: string | string[];
className: string;
notices: StoreNotice[];
} ): JSX.Element => {
Expand Down Expand Up @@ -64,12 +64,12 @@ const StoreNotices = ( {
} );
}
}, [ noticeIds, previousNoticeIds, ref ] );

// Register the container context with the parent.
useEffect( () => {
registerContainer( context );
const contexts = Array.isArray( context ) ? context : [ context ];
contexts.map( ( _context ) => registerContainer( _context ) );
return () => {
unregisterContainer( context );
contexts.map( ( _context ) => unregisterContainer( _context ) );
};
}, [ context, registerContainer, unregisterContainer ] );

Expand Down Expand Up @@ -117,6 +117,17 @@ const StoreNotices = ( {
if ( ! noticeGroup.length ) {
return null;
}
const uniqueNotices = noticeGroup.filter(
(
notice: Notice,
noticeIndex: number,
noticesArray: Notice[]
) =>
noticesArray.findIndex(
( _notice: Notice ) =>
_notice.content === notice.content
) === noticeIndex
);
return (
<Notice
key={ `store-notice-${ status }` }
Expand All @@ -130,7 +141,7 @@ const StoreNotices = ( {
} );
} }
>
{ noticeGroup.length === 1 ? (
{ uniqueNotices.length === 1 ? (
<>
{ sanitizeHTML(
decodeEntities(
Expand All @@ -140,7 +151,7 @@ const StoreNotices = ( {
</>
) : (
<ul>
{ noticeGroup.map( ( notice ) => (
{ uniqueNotices.map( ( notice ) => (
<li
key={
notice.id + '-' + notice.context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,79 @@ describe( 'StoreNoticesContainer', () => {
)
);
} );

it( 'Shows notices from several contexts', async () => {
dispatch( noticesStore ).createErrorNotice( 'Custom shipping error', {
id: 'custom-subcontext-test-error',
context: 'wc/checkout/shipping-address',
} );
dispatch( noticesStore ).createErrorNotice( 'Custom billing error', {
id: 'custom-subcontext-test-error',
context: 'wc/checkout/billing-address',
} );
render(
<StoreNoticesContainer
context={ [
'wc/checkout/billing-address',
'wc/checkout/shipping-address',
] }
/>
);
// This should match against 4 elements; A written and spoken message for each error.
expect( screen.getAllByText( /Custom shipping error/i ) ).toHaveLength(
2
);
expect( screen.getAllByText( /Custom billing error/i ) ).toHaveLength(
2
);
// 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'
)
);
} );

it( 'Combine same notices from several contexts', async () => {
dispatch( noticesStore ).createErrorNotice( 'Custom generic error', {
id: 'custom-subcontext-test-error',
context: 'wc/checkout/shipping-address',
} );
dispatch( noticesStore ).createErrorNotice( 'Custom generic error', {
id: 'custom-subcontext-test-error',
context: 'wc/checkout/billing-address',
} );
render(
<StoreNoticesContainer
context={ [
'wc/checkout/billing-address',
'wc/checkout/shipping-address',
] }
/>
);
// This should match against 2 elements; A written and spoken message.
expect( screen.getAllByText( /Custom generic error/i ) ).toHaveLength(
2
);
// 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'
)
);
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {

export interface StoreNoticesContainerProps {
className?: string | undefined;
context: string;
context: string | string[];
// List of additional notices that were added inline and not stored in the `core/notices` store.
additionalNotices?: ( NoticeType & NoticeOptions )[];
}
Expand Down

0 comments on commit 95702ed

Please sign in to comment.