Merge pull request #10249 from strapi/core/notification-api

Notfications API
This commit is contained in:
cyril lopez 2021-05-06 13:30:28 +02:00 committed by GitHub
commit 856e59e99e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 390 additions and 815 deletions

View File

@ -1,23 +1,34 @@
import React from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import { QueryClientProvider, QueryClient } from 'react-query';
import { ThemeProvider } from 'styled-components';
import configureStore from './core/store/configureStore';
import reducers from './reducers';
import basename from './utils/basename';
import LanguageProvider from './containers/LanguageProvider';
// TODO remove
import App from './containers/App';
import LanguageProvider from './containers/LanguageProvider';
import Fonts from './components/Fonts';
import GlobalStyle from './components/GlobalStyle';
import Notifications from './components/Notifications';
import themes from './themes';
import reducers from './reducers';
// TODO
import translationMessages from './translations';
// const App = () => 'todo';
window.strapi = {
backendURL: process.env.STRAPI_ADMIN_BACKEND_URL,
};
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
class StrapiApp {
plugins = {};
@ -39,14 +50,22 @@ class StrapiApp {
const store = configureStore(this);
return (
<Provider store={store}>
<Fonts />
<LanguageProvider messages={translationMessages}>
<BrowserRouter basename={basename}>
<App store={store} />
</BrowserRouter>
</LanguageProvider>
</Provider>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={themes}>
<GlobalStyle />
<Fonts />
<Provider store={store}>
<LanguageProvider messages={translationMessages}>
<>
<Notifications />
<BrowserRouter basename={basename}>
<App store={store} />
</BrowserRouter>
</>
</LanguageProvider>
</Provider>
</ThemeProvider>
</QueryClientProvider>
);
}
}

View File

@ -1,3 +1,9 @@
import 'sanitize.css/sanitize.css';
import 'bootstrap/dist/css/bootstrap.css';
import 'font-awesome/css/font-awesome.min.css';
import '@fortawesome/fontawesome-free/css/all.css';
// eslint-disable-next-line import/extensions
import '@fortawesome/fontawesome-free/js/all.min.js';
import { createGlobalStyle } from 'styled-components';
const GlobalStyle = createGlobalStyle`

View File

@ -1,168 +0,0 @@
import styled, { createGlobalStyle } from 'styled-components';
import { themePropTypes } from '@strapi/helper-plugin';
const GlobalNotification = createGlobalStyle`
.notificationIcon {
position: relative;
display: block;
width: 40px;
height: 40px;
> div {
position: absolute;
width: 20px;
height: 20px;
top: 10px;
left: 5px;
border-radius: 10px;
border: 1px solid ${props => props.theme.main.colors.green};
display: flex;
svg {
margin: auto;
color: ${props => props.theme.main.colors.green};
width: 10px;
height: 10px;
}
}
}
.notificationContent {
display: flex;
align-items: center;
width: 220px;
margin: 0;
padding-right: 10px;
border-right: 1px solid rgba(255, 255, 255, 0.3);
}
.notificationTitle {
margin-bottom: 0;
font-size: 1.4rem;
font-weight: 400;
line-height: 1.8rem;
}
.notificationClose {
position: relative;
display: flex;
width: 20px;
margin-right: 15px;
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;
}
}
.notificationSuccess{
.notificationIcon {
div {
border-color: ${props => props.theme.main.colors.green};
}
svg {
color: ${props => props.theme.main.colors.green};
}
}
}
.notificationWarning {
.notificationIcon {
div {
border-color: ${props => props.theme.main.colors.orange};
}
svg {
color: ${props => props.theme.main.colors.orange};
}
}
}
.notificationError {
.notificationIcon {
div {
border-color: ${props => props.theme.main.colors.red};
}
svg {
color: ${props => props.theme.main.colors.red};
}
}
}
.notificationInfo {
.notificationIcon {
div {
border-color: ${props => props.theme.main.colors.blue};
}
svg {
color: ${props => props.theme.main.colors.blue};
}
}
}
`;
const Li = styled.li`
position: relative;
display: flex;
align-items: center;
width: 300px;
min-height: 60px;
margin-bottom: 14px;
background: ${props => props.theme.main.colors.white};
border-radius: 2px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.15);
color: #333740;
transition: all 0.15s ease;
overflow: hidden;
z-index: 10;
padding: 1rem;
border-left: 2px solid ${props => props.theme.main.colors.green};
&.notificationError {
border-color: ${props => props.theme.main.colors.red};
}
&.notificationWarning {
border-color: ${props => props.theme.main.colors.orange};
}
&.notificationInfo {
border-color: ${props => props.theme.main.colors.blue};
}
&:last-child {
z-index: 1;
}
&:hover {
cursor: pointer;
box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2);
}
`;
Li.defaultProps = {
theme: {
main: {
colors: {
leftMenu: {},
},
sizes: {
header: {},
leftMenu: {},
},
},
},
};
Li.propTypes = {
...themePropTypes,
};
export default Li;
export { GlobalNotification };

View File

@ -1,104 +1,178 @@
/**
*
* Notification
*
*/
/* eslint-disable */
import React from 'react';
import React, { useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { isObject } from 'lodash';
import { Padded, Text, Flex } from '@buffetjs/core';
import { useIntl } from 'react-intl';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Remove } from '@buffetjs/icons';
import Li, { GlobalNotification } from './Li';
import { NotificationWrapper, IconWrapper, LinkArrow, RemoveWrapper } from './styledComponents';
class Notification extends React.Component {
// eslint-disable-line react/prefer-stateless-function
handleCloseClicked = () => {
this.props.onHideNotification(this.props.notification.id);
};
const types = {
success: {
icon: 'check',
color: 'green',
},
warning: {
icon: 'exclamation',
color: 'orange',
},
info: {
icon: 'info',
color: 'blue',
},
};
options = {
success: {
icon: 'check',
title: 'Success',
class: 'notificationSuccess',
},
warning: {
icon: 'exclamation',
title: 'Warning',
class: 'notificationWarning',
},
error: {
icon: 'exclamation',
title: 'Error',
class: 'notificationError',
},
info: {
icon: 'info',
title: 'Info',
class: 'notificationInfo',
},
};
const Notification = ({ dispatch, notification }) => {
const { formatMessage } = useIntl();
render() {
const options = this.options[this.props.notification.status] || this.options.info;
const {
notification: { message },
} = this.props;
const content =
isObject(message) && message.id ? (
<FormattedMessage id={message.id} defaultMessage={message.id} values={message.values} />
) : (
<FormattedMessage id={message} defaultMessage={message} />
);
const {
title,
message,
link,
type,
id,
onClose,
timeout,
blockTransition,
centered,
} = notification;
return (
<>
<GlobalNotification />
<Li
key={this.props.notification.id}
className={`${options.class}`}
onClick={this.handleCloseClicked}
>
<div className={`notificationIcon`}>
<div>
<FontAwesomeIcon icon={options.icon} />
</div>
</div>
<div className="notificationContent">
<p className="notificationTitle">{content}</p>
</div>
<div className={`notificationClose`}>
<Remove onClick={this.handleCloseClicked} />
</div>
</Li>
</>
);
}
}
const formattedMessage = msg => (typeof msg === 'string' ? msg : formatMessage(msg, msg.values));
const handleClose = useCallback(() => {
if (onClose) {
onClose();
}
dispatch({
type: 'HIDE_NOTIFICATION',
id,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
useEffect(() => {
let timeoutToClear;
if (!blockTransition) {
timeoutToClear = setTimeout(() => {
handleClose();
}, timeout || 2500);
}
return () => clearTimeout(timeoutToClear);
}, [blockTransition, handleClose, timeout]);
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>
);
};
Notification.defaultProps = {
notification: {
id: 1,
message: 'app.utils.defaultMessage',
status: 'success',
type: 'success',
message: {
id: 'notification.success.saved',
defaultMessage: 'Saved',
},
onClose: () => null,
timeout: 2500,
blockTransition: false,
centered: false,
},
};
Notification.propTypes = {
dispatch: PropTypes.func.isRequired,
notification: PropTypes.shape({
id: PropTypes.number,
message: PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape({
id: PropTypes.string,
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string,
values: PropTypes.object,
}),
]),
status: PropTypes.string,
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,
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string,
values: PropTypes.object,
}),
]).isRequired,
}),
type: PropTypes.string,
onClose: PropTypes.func,
timeout: PropTypes.number,
blockTransition: PropTypes.bool,
centered: PropTypes.bool,
}),
onHideNotification: PropTypes.func.isRequired,
};
export default Notification;

View File

@ -1,17 +0,0 @@
import styled from 'styled-components';
import { TransitionGroup } from 'react-transition-group';
const Wrapper = styled(TransitionGroup)`
position: fixed;
top: 72px;
left: 240px;
right: 0;
z-index: 1100;
list-style: none;
width: 300px;
margin: 0 auto;
overflow-y: hidden;
pointer-events: none;
`;
export default Wrapper;

View File

@ -1,56 +0,0 @@
/**
*
* NotificationsContainer
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { CSSTransition } from 'react-transition-group';
import Notification from '../Notification';
import Wrapper from './Wrapper';
/* eslint-disable */
const NotificationsContainer = ({ notifications, onHideNotification }) => {
if (notifications.length === 0) {
return false;
}
const notifs = notifications.map((notification, i) => (
<CSSTransition
key={i}
classNames="notification"
timeout={{
enter: 500,
exit: 300,
}}
>
<Notification
key={notification.id}
onHideNotification={onHideNotification}
notification={notification}
/>
</CSSTransition>
));
return <Wrapper>{notifs}</Wrapper>;
};
NotificationsContainer.defaultProps = {
notifications: [
{
id: 1,
message: 'app.utils.defaultMessage',
status: 'success',
},
],
};
NotificationsContainer.propTypes = {
notifications: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
onHideNotification: PropTypes.func.isRequired,
};
export default NotificationsContainer;

View File

@ -1,2 +1,46 @@
export { default as Notification } from './Notification';
export { default as NotificationsContainer } from './NotificationsContainer';
import React, { useEffect, useReducer } from 'react';
import { CSSTransition } from 'react-transition-group';
import Notification from './Notification';
import reducer, { initialState } from './reducer';
import NotificationsWrapper from './Wrapper';
const Notifications = () => {
const [{ notifications }, dispatch] = useReducer(reducer, initialState);
const displayNotification = config => {
dispatch({
type: 'SHOW_NOTIFICATION',
config,
});
};
useEffect(() => {
window.strapi = Object.assign(window.strapi || {}, {
notification: {
toggle: config => {
displayNotification(config);
},
},
});
}, []);
return (
<NotificationsWrapper>
{notifications.map(notification => (
<CSSTransition
key={notification.id}
classNames="notification"
timeout={{
enter: 500,
exit: 300,
}}
>
<Notification dispatch={dispatch} notification={notification} />
</CSSTransition>
))}
</NotificationsWrapper>
);
};
export default Notifications;

View File

@ -1,6 +1,5 @@
import produce from 'immer';
import { get } from 'lodash';
import { SHOW_NEW_NOTIFICATION, HIDE_NEW_NOTIFICATION } from './constants';
import get from 'lodash/get';
const initialState = {
notifId: 0,
@ -11,7 +10,7 @@ const notificationReducer = (state = initialState, action) =>
// eslint-disable-next-line consistent-return
produce(state, draftState => {
switch (action.type) {
case SHOW_NEW_NOTIFICATION: {
case 'SHOW_NOTIFICATION': {
draftState.notifications.push({
// No action.config spread to limit the notification API and avoid customization
id: state.notifId,
@ -31,7 +30,7 @@ const notificationReducer = (state = initialState, action) =>
draftState.notifId = state.notifId + 1;
break;
}
case HIDE_NEW_NOTIFICATION: {
case 'HIDE_NOTIFICATION': {
const indexToRemove = state.notifications.findIndex(notif => notif.id === action.id);
if (indexToRemove !== -1) {
@ -47,3 +46,4 @@ const notificationReducer = (state = initialState, action) =>
});
export default notificationReducer;
export { initialState };

View File

@ -1,7 +1,6 @@
import reducer from '../reducer';
import { SHOW_NEW_NOTIFICATION, HIDE_NEW_NOTIFICATION } from '../constants';
describe('AMDIN | CONTAINERS | NEWNOTIFICATION | reducer', () => {
describe('ADMIN | COMPONENTS | NOTIFICATIONS | reducer', () => {
describe('DEFAULT_ACTION', () => {
it('should return the initialState', () => {
const state = {
@ -12,10 +11,10 @@ describe('AMDIN | CONTAINERS | NEWNOTIFICATION | reducer', () => {
});
});
describe('SHOW_NEW_NOTIFICATION', () => {
describe('SHOW_NOTIFICATION', () => {
it('should add a notification', () => {
const action = {
type: SHOW_NEW_NOTIFICATION,
type: 'SHOW_NOTIFICATION',
config: {
type: 'success',
message: {
@ -49,10 +48,10 @@ describe('AMDIN | CONTAINERS | NEWNOTIFICATION | reducer', () => {
});
});
describe('HIDE_NEW_NOTIFICATION', () => {
describe('HIDE_NOTIFICATION', () => {
it('should remove a notification if the notification exist', () => {
const action = {
type: HIDE_NEW_NOTIFICATION,
type: 'HIDE_NOTIFICATION',
id: 1,
};
const initialState = {
@ -67,7 +66,7 @@ describe('AMDIN | CONTAINERS | NEWNOTIFICATION | reducer', () => {
it('should not remove the notification if the notification does not exist', () => {
const action = {
type: HIDE_NEW_NOTIFICATION,
type: 'HIDE_NOTIFICATION',
id: 3,
};
const initialState = {

View File

@ -2,13 +2,6 @@
*
* App.js
*
* This component is the skeleton around the actual pages, and should only
* contain code that should be seen on all pages. (e.g. navigation bar)
*
* NOTE: while this component should technically be a stateless functional
* component (SFC), hot reloading does not currently support SFCs. If hot
* reloading is not a neccessity for you then you can refactor it and remove
* the linting exception.
*/
import React, { useEffect, useRef, useState, useMemo } from 'react';
@ -17,50 +10,24 @@ import { Switch, Route } from 'react-router-dom';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { LoadingIndicatorPage, auth, request } from '@strapi/helper-plugin';
import { QueryClientProvider, QueryClient } from 'react-query';
// FIXME
import 'sanitize.css/sanitize.css';
import 'bootstrap/dist/css/bootstrap.css';
import 'font-awesome/css/font-awesome.min.css';
import '@fortawesome/fontawesome-free/css/all.css';
// eslint-disable-next-line import/extensions
import '@fortawesome/fontawesome-free/js/all.min.js';
import GlobalStyle from '../../components/GlobalStyle';
import Admin from '../Admin';
import AuthPage from '../AuthPage';
import NotFoundPage from '../NotFoundPage';
// FIXME
// eslint-disable-next-line import/no-cycle
import NotificationProvider from '../NotificationProvider';
import Theme from '../Theme';
import { getUID } from './utils';
import { Content, Wrapper } from './components';
import { getDataSucceeded } from './actions';
import NewNotification from '../NewNotification';
import PrivateRoute from '../PrivateRoute';
import routes from './utils/routes';
import { makeUniqueRoutes, createRoute } from '../SettingsPage/utils';
window.strapi = Object.assign(window.strapi || {}, {
notification: {
toggle: () => {},
},
lockApp: () => console.log('todo lockApp'),
unlockApp: () => console.log('todo unlockApp'),
lockAppWithOverlay: () => console.log('todo unlockAppWithOverlay'),
});
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
function App(props) {
const getDataRef = useRef();
const [{ isLoading, hasAdmin }, setState] = useState({ isLoading: true, hasAdmin: false });
@ -143,29 +110,22 @@ function App(props) {
}
return (
<Theme>
<Wrapper>
<GlobalStyle />
<QueryClientProvider client={queryClient}>
<NotificationProvider />
<NewNotification />
<Content>
<Switch>
{authRoutes}
<Route
path="/auth/:authType"
render={routerProps => (
<AuthPage {...routerProps} setHasAdmin={setHasAdmin} hasAdmin={hasAdmin} />
)}
exact
/>
<PrivateRoute path="/" component={Admin} />
<Route path="" component={NotFoundPage} />
</Switch>
</Content>
</QueryClientProvider>
</Wrapper>
</Theme>
<Wrapper>
<Content>
<Switch>
{authRoutes}
<Route
path="/auth/:authType"
render={routerProps => (
<AuthPage {...routerProps} setHasAdmin={setHasAdmin} hasAdmin={hasAdmin} />
)}
exact
/>
<PrivateRoute path="/" component={Admin} />
<Route path="" component={NotFoundPage} />
</Switch>
</Content>
</Wrapper>
);
}

View File

@ -1,181 +0,0 @@
import React, { useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import { Padded, Text, Flex } from '@buffetjs/core';
import { useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Remove } from '@buffetjs/icons';
import { HIDE_NEW_NOTIFICATION } from '../constants';
import { NotificationWrapper, IconWrapper, LinkArrow, RemoveWrapper } from './styledComponents';
const types = {
success: {
icon: 'check',
color: 'green',
},
warning: {
icon: 'exclamation',
color: 'orange',
},
info: {
icon: 'info',
color: 'blue',
},
};
const Notification = ({ notification }) => {
const { formatMessage } = useIntl();
const dispatch = useDispatch();
const {
title,
message,
link,
type,
id,
onClose,
timeout,
blockTransition,
centered,
} = notification;
const formattedMessage = msg => (typeof msg === 'string' ? msg : formatMessage(msg, msg.values));
const handleClose = useCallback(() => {
if (onClose) {
onClose();
}
dispatch({
type: HIDE_NEW_NOTIFICATION,
id,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
useEffect(() => {
let timeoutToClear;
if (!blockTransition) {
timeoutToClear = setTimeout(() => {
handleClose();
}, timeout || 2500);
}
return () => clearTimeout(timeoutToClear);
}, [blockTransition, handleClose, timeout]);
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>
);
};
Notification.defaultProps = {
notification: {
id: 1,
type: 'success',
message: {
id: 'notification.success.saved',
defaultMessage: 'Saved',
},
onClose: () => null,
timeout: 2500,
blockTransition: false,
centered: false,
},
};
Notification.propTypes = {
notification: PropTypes.shape({
id: PropTypes.number,
message: PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string,
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,
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string,
values: PropTypes.object,
}),
]).isRequired,
}),
type: PropTypes.string,
onClose: PropTypes.func,
timeout: PropTypes.number,
blockTransition: PropTypes.bool,
centered: PropTypes.bool,
}),
};
export default Notification;

View File

@ -1,9 +0,0 @@
/* eslint-disable import/prefer-default-export */
import { SHOW_NEW_NOTIFICATION } from './constants';
export function showNotification(config) {
return {
type: SHOW_NEW_NOTIFICATION,
config,
};
}

View File

@ -1,2 +0,0 @@
export const SHOW_NEW_NOTIFICATION = 'SHOW_NEW_NOTIFICATION';
export const HIDE_NEW_NOTIFICATION = 'HIDE_NEW_NOTIFICATION';

View File

@ -1,35 +0,0 @@
/**
*
* NotificationsContainer
*
*/
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import Notification from './Notification';
import NotificationsWrapper from './Wrapper';
const NotificationsContainer = () => {
const notifications = useSelector(state => state.newNotification.notifications);
return (
<NotificationsWrapper>
{notifications.map(notification => (
<CSSTransition
key={notification.id}
classNames="notification"
timeout={{
enter: 500,
exit: 300,
}}
>
<Notification notification={notification} />
</CSSTransition>
))}
</NotificationsWrapper>
);
};
export default memo(NotificationsContainer);

View File

@ -1,40 +0,0 @@
/*
*
* NotificationProvider actions
*
*/
/* eslint-disable import/no-cycle */
// import { dispatch } from '../../app';
import { SHOW_NOTIFICATION, HIDE_NOTIFICATION } from './constants';
// TODO
const dispatch = () => {};
let nextNotificationId = 0;
export function showNotification(message, status) {
nextNotificationId++; // eslint-disable-line no-plusplus
// Start timeout to hide the notification
(id => {
setTimeout(() => {
dispatch(hideNotification(id));
}, 2500);
})(nextNotificationId);
return {
type: SHOW_NOTIFICATION,
message,
status,
id: nextNotificationId,
};
}
export function hideNotification(id) {
return {
type: HIDE_NOTIFICATION,
id,
};
}

View File

@ -1,8 +0,0 @@
/*
*
* NotificationProvider constants
*
*/
export const SHOW_NOTIFICATION = 'app/NotificationProvider/SHOW_NOTIFICATION';
export const HIDE_NOTIFICATION = 'app/NotificationProvider/HIDE_NOTIFICATION';

View File

@ -1,45 +0,0 @@
/*
*
* NotificationProvider
*
*/
/* eslint-disable */
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { NotificationsContainer } from '../../components/Notifications';
import { selectNotifications } from './selectors';
import { hideNotification } from './actions';
export class NotificationProvider extends React.Component {
render() {
return (
<NotificationsContainer
onHideNotification={this.props.onHideNotification}
notifications={this.props.notifications}
/>
);
}
}
NotificationProvider.propTypes = {
notifications: PropTypes.object.isRequired,
onHideNotification: PropTypes.func.isRequired,
};
const mapStateToProps = createStructuredSelector({
notifications: selectNotifications(),
});
function mapDispatchToProps(dispatch) {
return {
onHideNotification: id => {
dispatch(hideNotification(id));
},
dispatch,
};
}
export default connect(mapStateToProps, mapDispatchToProps)(NotificationProvider);

View File

@ -1,48 +0,0 @@
/*
*
* NotificationProvider reducer
*
*/
import { fromJS } from 'immutable';
import { SHOW_NOTIFICATION, HIDE_NOTIFICATION } from './constants';
const initialState = fromJS({
notifications: [],
});
function notificationProviderReducer(state = initialState, action) {
// Init variable
let index;
switch (action.type) {
case SHOW_NOTIFICATION:
return state.set(
'notifications',
state.get('notifications').push({
message: action.message || 'app.utils.defaultMessage',
status: action.status || 'success',
id: action.id,
})
);
case HIDE_NOTIFICATION:
// Check that the index exists
state.get('notifications').forEach((notification, i) => {
if (notification.id === action.id) {
index = i;
}
});
if (typeof index !== 'undefined') {
// Remove the notification
return state.set('notifications', state.get('notifications').splice(index, 1));
}
// Notification not found, return the current state
return state;
default:
return state;
}
}
export default notificationProviderReducer;

View File

@ -1,27 +0,0 @@
import { createSelector } from 'reselect';
/**
* Direct selector to the notificationProvider state domain
*/
const selectNotificationProviderDomain = () => state => state.notification;
/**
* Other specific selectors
*/
/**
* Default selector used by NotificationProvider
*/
const selectNotificationProvider = () =>
createSelector(selectNotificationProviderDomain(), notificationProviderState =>
notificationProviderState.toJS()
);
const selectNotifications = () =>
createSelector(selectNotificationProviderDomain(), notificationProviderState =>
notificationProviderState.get('notifications')
);
export default selectNotificationProvider;
export { selectNotificationProviderDomain, selectNotifications };

View File

@ -1,14 +0,0 @@
import React from 'react';
import { ThemeProvider } from 'styled-components';
import PropTypes from 'prop-types';
import themes from '../../themes';
const Theme = ({ children }) => {
return <ThemeProvider theme={themes}>{children}</ThemeProvider>;
};
Theme.propTypes = {
children: PropTypes.node.isRequired,
};
export default Theme;

View File

@ -1,8 +1,6 @@
import globalReducer from '../containers/App/reducer';
import adminReducer from '../containers/Admin/reducer';
import languageProviderReducer from '../containers/LanguageProvider/reducer';
import notificationProviderReducer from '../containers/NotificationProvider/reducer';
import newNotificationReducer from '../containers/NewNotification/reducer';
import permissionsManagerReducer from '../containers/PermissionsManager/reducer';
import menuReducer from '../containers/LeftMenu/reducer';
@ -12,8 +10,6 @@ const reducers = {
app: globalReducer,
admin: adminReducer,
language: languageProviderReducer,
notification: notificationProviderReducer,
newNotification: newNotificationReducer,
permissionsManager: permissionsManagerReducer,
menu: menuReducer,
};

View File

@ -5,6 +5,133 @@ describe('ADMIN | StrapiApp', () => {
it('should render the app without plugins', () => {
const app = StrapiApp({});
render(app.render());
expect(render(app.render())).toMatchInlineSnapshot(`
Object {
"asFragment": [Function],
"baseElement": .c1 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: space-around;
-webkit-justify-content: space-around;
-ms-flex-pack: space-around;
justify-content: space-around;
width: 100%;
height: 100vh;
}
.c1 > div {
margin: auto;
width: 50px;
height: 50px;
border: 6px solid #f3f3f3;
border-top: 6px solid #1c91e7;
border-radius: 50%;
-webkit-animation: fEWCgj 2s linear infinite;
animation: fEWCgj 2s linear infinite;
}
.c0 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
position: fixed;
top: 72px;
left: 0;
right: 0;
z-index: 1100;
list-style: none;
width: 100%;
overflow-y: hidden;
pointer-events: none;
}
<body>
<div>
<div
class="c0"
/>
<div
class="c1"
>
<div />
</div>
</div>
</body>,
"container": <div>
<div
class="sc-fTxPal krAKpM"
/>
<div
class="Loader-sc-1xt0x01-0 JXMaR"
>
<div />
</div>
</div>,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`);
});
});