Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[@wordpress/notices] Add removeAllNotices action to allow all notices to be removed from a given context #44059

Merged
merged 9 commits into from
May 30, 2023
47 changes: 47 additions & 0 deletions docs/reference-guides/data/data-core-notices.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,53 @@ _Returns_

- `Object`: Action object.

### removeAllNotices

Removes all notices from a given context. Defaults to the default context.

_Usage_

```js
import { __ } from '@wordpress/i18n';
import { useDispatch, useSelect } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import { Button } from '@wordpress/components';

export const ExampleComponent = () => {
const notices = useSelect( ( select ) =>
select( noticesStore ).getNotices()
);
const { removeNotices } = useDispatch( noticesStore );
return (
<>
<ul>
{ notices.map( ( notice ) => (
<li key={ notice.id }>{ notice.content }</li>
) ) }
</ul>
<Button onClick={ () => removeAllNotices() }>
{ __( 'Clear all notices', 'woo-gutenberg-products-block' ) }
</Button>
<Button onClick={ () => removeAllNotices( 'snackbar' ) }>
{ __(
'Clear all snackbar notices',
'woo-gutenberg-products-block'
) }
</Button>
</>
);
};
```

_Parameters_

- _noticeType_ `string`: The context to remove all notices from.
- _context_ `string`: The context to remove all notices from.

_Returns_

- `Object`: Action object.

### removeNotice

Returns an action object used in signalling that a notice is to be removed.
Expand Down
1 change: 1 addition & 0 deletions packages/notices/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### New Feature

- Add a new action `removeNotices` which allows bulk removal of notices by their IDs. ([#39940](https://github.com/WordPress/gutenberg/pull/39940))
- Add a new action `removeAllNotices` which removes all notices from a given context. ([#44059](https://github.com/WordPress/gutenberg/pull/44059))

## 4.2.0 (2023-05-24)

Expand Down
57 changes: 57 additions & 0 deletions packages/notices/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,63 @@ export function removeNotice( id, context = DEFAULT_CONTEXT ) {
};
}

/**
* Removes all notices from a given context. Defaults to the default context.
*
* @param {string} noticeType The context to remove all notices from.
* @param {string} context The context to remove all notices from.
*
* @example
* ```js
* import { __ } from '@wordpress/i18n';
* import { useDispatch, useSelect } from '@wordpress/data';
* import { store as noticesStore } from '@wordpress/notices';
* import { Button } from '@wordpress/components';
*
* export const ExampleComponent = () => {
* const notices = useSelect( ( select ) =>
* select( noticesStore ).getNotices()
* );
* const { removeNotices } = useDispatch( noticesStore );
* return (
* <>
* <ul>
* { notices.map( ( notice ) => (
* <li key={ notice.id }>{ notice.content }</li>
* ) ) }
* </ul>
* <Button
* onClick={ () =>
* removeAllNotices()
* }
* >
* { __( 'Clear all notices', 'woo-gutenberg-products-block' ) }
* </Button>
* <Button
* onClick={ () =>
* removeAllNotices( 'snackbar' )
* }
* >
* { __( 'Clear all snackbar notices', 'woo-gutenberg-products-block' ) }
* </Button>
* </>
* );
* };
* ```
*
* @return {Object} Action object.
*/
export function removeAllNotices(
noticeType = 'default',
context = DEFAULT_CONTEXT
) {
return {
type: 'REMOVE_ALL_NOTICES',
noticeType,
context,
};
}

/**
* Returns an action object used in signalling that several notices are to be removed.
*
Expand Down
3 changes: 3 additions & 0 deletions packages/notices/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ const notices = onSubKey( 'context' )( ( state = [], action ) => {

case 'REMOVE_NOTICES':
return state.filter( ( { id } ) => ! action.ids.includes( id ) );

case 'REMOVE_ALL_NOTICES':
return state.filter( ( { type } ) => type !== action.noticeType );
Comment on lines +30 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't considering the provided context in the action object. Should it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't considering the provided context in the action object. Should it?

The onSubKey in the reducer takes care of that actually.

I have tests that cover removing notices from a specific context: https://github.com/wordpress/gutenberg/blob/2a5db119b8ed8e16002de157a5edf37ff47dffe6/packages/notices/src/store/test/reducer.js#L230

}

return state;
Expand Down
29 changes: 29 additions & 0 deletions packages/notices/src/store/test/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
createErrorNotice,
createWarningNotice,
removeNotice,
removeAllNotices,
removeNotices,
} from '../actions';
import { DEFAULT_CONTEXT, DEFAULT_STATUS } from '../constants';
Expand Down Expand Up @@ -239,4 +240,32 @@ describe( 'actions', () => {
} );
} );
} );

describe( 'removeAllNotices', () => {
it( 'should return action', () => {
expect( removeAllNotices() ).toEqual( {
type: 'REMOVE_ALL_NOTICES',
noticeType: 'default',
context: DEFAULT_CONTEXT,
} );
} );

it( 'should return action with custom context', () => {
const context = 'foo';

expect( removeAllNotices( 'default', context ) ).toEqual( {
type: 'REMOVE_ALL_NOTICES',
noticeType: 'default',
context,
} );
} );

it( 'should return action with type', () => {
expect( removeAllNotices( 'snackbar' ) ).toEqual( {
type: 'REMOVE_ALL_NOTICES',
noticeType: 'snackbar',
context: DEFAULT_CONTEXT,
} );
} );
} );
} );
87 changes: 86 additions & 1 deletion packages/notices/src/store/test/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import deepFreeze from 'deep-freeze';
* Internal dependencies
*/
import reducer from '../reducer';
import { createNotice, removeNotice, removeNotices } from '../actions';
import {
createNotice,
removeNotice,
removeNotices,
removeAllNotices,
} from '../actions';
import { getNotices } from '../selectors';
import { DEFAULT_CONTEXT } from '../constants';

Expand Down Expand Up @@ -208,4 +213,84 @@ describe( 'reducer', () => {
],
} );
} );

it( 'should remove all notices', () => {
let action = createNotice( 'error', 'save error' );
const original = deepFreeze( reducer( undefined, action ) );

action = createNotice( 'success', 'successfully saved' );
let state = reducer( original, action );
state = reducer( state, removeAllNotices() );

expect( state ).toEqual( {
[ DEFAULT_CONTEXT ]: [],
} );
} );

it( 'should remove all notices in a given context but leave other contexts intact', () => {
let action = createNotice( 'error', 'save error', {
context: 'foo',
id: 'foo-error',
} );
const original = deepFreeze( reducer( undefined, action ) );

action = createNotice( 'success', 'successfully saved', {
context: 'bar',
} );

let state = reducer( original, action );
state = reducer( state, removeAllNotices( 'default', 'bar' ) );

expect( state ).toEqual( {
bar: [],
foo: [
{
id: 'foo-error',
content: 'save error',
spokenMessage: 'save error',
__unstableHTML: undefined,
status: 'error',
isDismissible: true,
actions: [],
type: 'default',
icon: null,
explicitDismiss: false,
onDismiss: undefined,
},
],
} );
} );

it( 'should remove all notices of a given type', () => {
let action = createNotice( 'error', 'save error', {
id: 'global-error',
} );
const original = deepFreeze( reducer( undefined, action ) );

action = createNotice( 'success', 'successfully saved', {
type: 'snackbar',
id: 'snackbar-success',
} );

let state = reducer( original, action );
state = reducer( state, removeAllNotices( 'default' ) );

expect( state ).toEqual( {
[ DEFAULT_CONTEXT ]: [
{
id: 'snackbar-success',
content: 'successfully saved',
spokenMessage: 'successfully saved',
__unstableHTML: undefined,
status: 'success',
isDismissible: true,
actions: [],
type: 'snackbar',
icon: null,
explicitDismiss: false,
onDismiss: undefined,
},
],
} );
} );
} );