mirror of
https://github.com/strapi/strapi.git
synced 2025-12-27 23:24:03 +00:00
Merge pull request #10797 from strapi/core/notifications
Migration : admin notifications
This commit is contained in:
commit
903d07b4c5
@ -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,
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@ -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 };
|
||||
@ -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;
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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
@ -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",
|
||||
|
||||
@ -41,7 +41,6 @@ const useAuthProviders = ({ ssoEnabled }) => {
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
message: { id: 'notification.error' },
|
||||
centered: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
```
|
||||
Loading…
x
Reference in New Issue
Block a user