Merge pull request #10797 from strapi/core/notifications

Migration : admin notifications
This commit is contained in:
ELABBASSI Hicham 2021-08-26 17:37:29 +02:00 committed by GitHub
commit 903d07b4c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 326 additions and 242 deletions

View File

@ -1,40 +1,12 @@
import React, { useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import { Padded, Text, Flex } from '@buffetjs/core';
import { useIntl } from 'react-intl';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Remove } from '@buffetjs/icons';
import { NotificationWrapper, IconWrapper, LinkArrow, RemoveWrapper } from './styledComponents';
const types = {
success: {
icon: 'check',
color: 'green',
},
warning: {
icon: 'exclamation',
color: 'orange',
},
info: {
icon: 'info',
color: 'blue',
},
};
import { Alert } from '@strapi/parts/Alert';
import { Link } from '@strapi/parts/Link';
const Notification = ({ dispatch, notification }) => {
const { formatMessage } = useIntl();
const {
title,
message,
link,
type,
id,
onClose,
timeout,
blockTransition,
centered,
} = notification;
const { message, link, type, id, onClose, timeout, blockTransition } = notification;
const formattedMessage = msg => (typeof msg === 'string' ? msg : formatMessage(msg, msg.values));
const handleClose = useCallback(() => {
@ -61,62 +33,53 @@ const Notification = ({ dispatch, notification }) => {
return () => clearTimeout(timeoutToClear);
}, [blockTransition, handleClose, timeout]);
let variant;
let alertTitle;
if (type === 'info') {
variant = 'default';
alertTitle = formatMessage({
id: 'notification.default.title',
defaultMessage: 'Information Alert:',
});
} else if (type === 'warning') {
alertTitle = formatMessage({
id: 'notification.warning.title',
defaultMessage: 'Warning Alert:',
});
variant = 'danger';
} else {
alertTitle = formatMessage({
id: 'notification.success.title',
defaultMessage: 'Success Alert:',
});
variant = 'success';
}
return (
<NotificationWrapper centered={centered} color={types[type].color}>
<Padded top left right bottom size="smd">
<Flex alignItems="center" justifyContent="space-between">
<IconWrapper>
<FontAwesomeIcon icon={types[type].icon} />
</IconWrapper>
<Padded left size="sm" style={{ width: '80%', flex: 1 }}>
{title && (
<Text
fontSize="xs"
textTransform="uppercase"
color="grey"
title={formattedMessage(title)}
>
{formattedMessage(title)}
</Text>
)}
<Flex justifyContent="space-between">
{message && (
<Text title={formattedMessage(message)}>{formattedMessage(message)}</Text>
)}
{link && (
<a
href={link.url}
target={link.target || '_blank'}
rel={!link.target || link.target === '_blank' ? 'noopener noreferrer' : ''}
>
<Padded right left size="xs">
<Flex alignItems="center">
<Text
style={{ maxWidth: '100px' }}
ellipsis
fontWeight="bold"
color="blue"
title={formattedMessage(link.label)}
>
{formattedMessage(link.label)}
</Text>
{link.target === '_blank' && (
<Padded left size="xs">
<LinkArrow />
</Padded>
)}
</Flex>
</Padded>
</a>
)}
</Flex>
</Padded>
<RemoveWrapper>
<Remove onClick={handleClose} />
</RemoveWrapper>
</Flex>
</Padded>
</NotificationWrapper>
<Alert
action={
link ? (
<Link href={link.url} target="_blank">
{formatMessage({
id: link.label?.id || link.label,
defaultMessage: link.label?.defaultMessage || link.label?.id || link.label,
})}
</Link>
) : (
undefined
)
}
onClose={handleClose}
closeLabel="Close"
title={alertTitle}
variant={variant}
>
{formattedMessage({
id: message?.id || message,
defaultMessage: message?.defaultMessage || message?.id || message,
})}
</Alert>
);
};
@ -131,7 +94,6 @@ Notification.defaultProps = {
onClose: () => null,
timeout: 2500,
blockTransition: false,
centered: false,
},
};
@ -147,14 +109,6 @@ Notification.propTypes = {
values: PropTypes.object,
}),
]),
title: PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string,
values: PropTypes.object,
}),
]),
link: PropTypes.shape({
target: PropTypes.string,
url: PropTypes.string.isRequired,
@ -171,7 +125,6 @@ Notification.propTypes = {
onClose: PropTypes.func,
timeout: PropTypes.number,
blockTransition: PropTypes.bool,
centered: PropTypes.bool,
}),
};

View File

@ -1,68 +0,0 @@
import styled from 'styled-components';
import { Arrow } from '@buffetjs/icons';
const NotificationWrapper = styled.div`
position: relative;
pointer-events: auto;
&:hover {
box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2);
}
`;
// border-top-right-radius: ${({ theme }) => theme.main.sizes.borderRadius};
// border-bottom-right-radius: ${({ theme }) => theme.main.sizes.borderRadius};
// margin-bottom: ${({ theme }) => theme.main.sizes.paddings.sm};
// box-shadow: 0 2px 4px 0 ${({ theme }) => theme.main.colors.darkGrey};
// background-color: ${props => props.theme.main.colors.white};
// border-left: 2px solid ${({ theme, color }) => theme.main.colors[color]};
// overflow: hidden;
// z-index: 10;
// color: ${({ color, theme }) => theme.main.colors[color]};
// transition: all 0.15s ease;
// width: 400px;
// min-height: 60px;
// margin-left: ${({ centered }) => (centered ? '0px' : '240px')};
const IconWrapper = styled.div`
border: 1px solid;
padding: 5px;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
`;
const LinkArrow = styled(Arrow)`
transform: rotate(45deg);
margin-top: 4px;
color: ${({ theme }) => theme.main.colors.blue};
`;
const RemoveWrapper = styled.div`
position: relative;
display: flex;
width: 20px;
cursor: pointer;
opacity: 0.6;
font-size: 1.4rem;
color: #bbc2bf;
transition: opacity 0.1s ease;
-webkit-font-smoothing: antialiased;
&:hover {
opacity: 1;
}
svg {
margin: auto;
font-size: 1.3rem;
font-weight: 100 !important;
}
`;
export { NotificationWrapper, IconWrapper, LinkArrow, RemoveWrapper };

View File

@ -1,19 +1,12 @@
import styled from 'styled-components';
import { TransitionGroup } from 'react-transition-group';
import { Row } from '@strapi/parts';
const Wrapper = styled(TransitionGroup)`
display: flex;
align-items: center;
flex-direction: column;
const Wrapper = styled(Row)`
position: fixed;
top: 72px;
left: 0;
top: 46px;
right: 0;
left: 0;
z-index: 1100;
list-style: none;
width: 100%;
overflow-y: hidden;
pointer-events: none;
`;
export default Wrapper;

View File

@ -1,15 +1,14 @@
import { NotificationsProvider } from '@strapi/helper-plugin';
import React, { useReducer } from 'react';
import PropTypes from 'prop-types';
// import { CSSTransition } from 'react-transition-group';
// import Notification from './Notification';
import { Box } from '@strapi/parts/Box';
import { Stack } from '@strapi/parts/Stack';
import Notification from './Notification';
import reducer, { initialState } from './reducer';
// import NotificationsWrapper from './Wrapper';
import NotificationsWrapper from './Wrapper';
const Notifications = ({ children }) => {
// const [{ notifications }, dispatch] = useReducer(reducer, initialState);
const [, dispatch] = useReducer(reducer, initialState);
const [{ notifications }, dispatch] = useReducer(reducer, initialState);
const displayNotification = config => {
dispatch({
@ -20,30 +19,20 @@ const Notifications = ({ children }) => {
return (
<NotificationsProvider toggleNotification={displayNotification}>
<NotificationsWrapper justifyContent="space-around">
<Stack size={notifications.length}>
{notifications.map(notification => {
return (
<Box key={notification.id} style={{ width: 500 }}>
<Notification dispatch={dispatch} notification={notification} />
</Box>
);
})}
</Stack>
</NotificationsWrapper>
{children}
</NotificationsProvider>
);
// FIXME
// return (
// <NotificationsProvider toggleNotification={displayNotification}>
// <NotificationsWrapper>
// {notifications.map(notification => (
// <CSSTransition
// key={notification.id}
// classNames="notification"
// timeout={{
// enter: 500,
// exit: 300,
// }}
// >
// <Notification dispatch={dispatch} notification={notification} />
// </CSSTransition>
// ))}
// </NotificationsWrapper>
// {children}
// </NotificationsProvider>
// );
};
Notifications.propTypes = {

View File

@ -19,13 +19,10 @@ const notificationReducer = (state = initialState, action) =>
id: 'notification.success.saved',
defaultMessage: 'Saved',
}),
title: get(action, ['config', 'title'], null),
link: get(action, ['config', 'link'], null),
timeout: get(action, ['config', 'timeout'], 2500),
blockTransition: get(action, ['config', 'blockTransition'], false),
uid: get(action, ['config', 'uid'], null),
onClose: get(action, ['config', 'onClose'], null),
centered: get(action, ['config', 'centered'], false),
});
draftState.notifId = state.notifId + 1;
break;

View File

@ -0,0 +1,176 @@
/**
*
* Tests for Notifications
*
*/
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import { useNotification } from '@strapi/helper-plugin';
import { act } from 'react-dom/test-utils';
import Theme from '../../Theme';
import Notifications from '../index';
const messages = {
en: {},
};
describe('<Notifications />', () => {
it('renders and matches the snapshot', () => {
const {
container: { firstChild },
} = render(
<Theme>
<IntlProvider locale="en" messages={messages} textComponent="span">
<Notifications>
<div />
</Notifications>
</IntlProvider>
</Theme>
);
expect(firstChild).toMatchInlineSnapshot(`
.c0 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-pack: space-around;
-webkit-justify-content: space-around;
-ms-flex-pack: space-around;
justify-content: space-around;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c2 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
.c2 > * {
margin-top: 0;
margin-bottom: 0;
}
.c2 > * + * {
margin-top: 0px;
}
.c1 {
position: fixed;
top: 46px;
right: 0;
left: 0;
z-index: 1100;
}
<div
class="c0 c1"
>
<div
class="c2"
/>
</div>
`);
});
it('should display a notification correctly', async () => {
const Button = () => {
const toggleNotification = useNotification();
const handleClick = () => {
toggleNotification({ type: 'success', message: 'simple notif' });
};
return (
<button onClick={handleClick} type="button">
display notif
</button>
);
};
render(
<Theme>
<IntlProvider locale="en" messages={messages} textComponent="span">
<Notifications>
<Button />
</Notifications>
</IntlProvider>
</Theme>
);
// Click button
fireEvent.click(screen.getByText('display notif'));
const items = await screen.findAllByText(/simple notif/);
expect(items).toHaveLength(1);
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 2500));
});
const foundItems = screen.queryAllByText(/simple notif/);
expect(foundItems).toHaveLength(0);
});
it('should display a notification correctly and not toggle it', async () => {
const Button = () => {
const toggleNotification = useNotification();
const handleClick = () => {
toggleNotification({ type: 'success', message: 'simple notif', blockTransition: true });
};
return (
<button onClick={handleClick} type="button">
display notif
</button>
);
};
render(
<Theme>
<IntlProvider locale="en" messages={messages} textComponent="span">
<Notifications>
<Button />
</Notifications>
</IntlProvider>
</Theme>
);
// Click button
fireEvent.click(screen.getByText('display notif'));
const items = await screen.findAllByText(/simple notif/);
expect(items).toHaveLength(1);
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 2500));
});
const foundItems = screen.queryAllByText(/simple notif/);
expect(foundItems).toHaveLength(1);
fireEvent.click(screen.getByLabelText('Close'));
const displayedItems = screen.queryAllByText(/simple notif/);
expect(displayedItems).toHaveLength(0);
});
});

View File

@ -32,12 +32,9 @@ describe('ADMIN | COMPONENTS | NOTIFICATIONS | reducer', () => {
id: 0,
type: 'success',
message: { id: 'notification.message' },
title: null,
link: null,
timeout: 2500,
blockTransition: false,
centered: false,
uid: null,
onClose: null,
},
],

File diff suppressed because one or more lines are too long

View File

@ -576,6 +576,9 @@
"notification.form.success.fields": "Changes saved",
"notification.link-copied": "Link copied into the clipboard",
"notification.permission.not-allowed-read": "You are not allowed to see this document",
"notification.default.title": "Information Alert:",
"notification.success.title": "Success Alert:",
"notification.warning.title": "Warning Alert:",
"notification.success.delete": "The item has been deleted",
"notification.success.saved": "Saved",
"notification.version.update.link": "See more",

View File

@ -41,7 +41,6 @@ const useAuthProviders = ({ ssoEnabled }) => {
toggleNotification({
type: 'warning',
message: { id: 'notification.error' },
centered: true,
});
}
};

View File

@ -0,0 +1,48 @@
<!--- useNotification.stories.mdx --->
import { Meta } from '@storybook/addon-docs';
<Meta title="hooks/useNotification" />
# useNotification
This hook is used in order to display a notification in the admin panel.
## Usage
```
import { useNotification } from '@strapi/helper-plugin';
import { Button, Main } from '@strapi/parts';
const HomePage = () => {
const toggleNotification = useNotification();
const handleClick = () => {
toggleNotification({
// required
type: 'info|success|warning',
// required
message: { id: 'notification.version.update.message', defaultMessage: 'A new version is available' },
// optional
link: {
url: 'https://github.com/strapi/strapi/releases/tag/v4',
label: {
id: 'notification.version.update.link',
defaultMessage: 'See more'
},
},
// optional: default = false
blockTransition: true,
// optional
onClose: () => localStorage.setItem('STRAPI_UPDATE_NOTIF', true),
});
}
return (
<Main>
<h1>This is the homepage</h1>
<Button onClick={handleClick}>Display notification</Button>
</Main>
);
};
```