Undo old notif code and add new notif

Signed-off-by: HichamELBSI <elabbassih@gmail.com>
This commit is contained in:
HichamELBSI 2020-10-09 17:57:07 +02:00
parent ab6fb45c17
commit 4add565de4
21 changed files with 637 additions and 234 deletions

View File

@ -32,7 +32,8 @@ import { StrapiProvider } from 'strapi-helper-plugin';
import { merge } from 'lodash';
import Fonts from './components/Fonts';
import { freezeApp, pluginLoaded, unfreezeApp, updatePlugin } from './containers/App/actions';
import { showNotification, showNewNotification } from './containers/NotificationProvider/actions';
import { showNotification } from './containers/NotificationProvider/actions';
import { showNotification as showNewNotification } from './containers/NewNotification/actions';
import basename from './utils/basename';
import getInjectors from './utils/reducerInjectors';

View File

@ -0,0 +1,168 @@
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

@ -6,106 +6,84 @@
/* eslint-disable */
import React from 'react';
import PropTypes from 'prop-types';
import { Padded, Text, Flex } from '@buffetjs/core';
import { useIntl } from 'react-intl';
import { FormattedMessage } from 'react-intl';
import { isObject } from 'lodash';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Remove } from '@buffetjs/icons';
import Li, { GlobalNotification } from './Li';
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, onHideNotification }) => {
const { formatMessage } = useIntl();
const { title, message, link, type, id, onClose } = notification;
const formattedMessage = formatMessage(typeof message === 'string' ? { id: message } : message);
const handleClose = () => {
if (onClose) {
onClose();
}
onHideNotification(id);
class Notification extends React.Component {
// eslint-disable-line react/prefer-stateless-function
handleCloseClicked = () => {
this.props.onHideNotification(this.props.notification.id);
};
return (
<NotificationWrapper 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={formatMessage(title)}
>
{formatMessage(title)}
</Text>
)}
<Flex>
{message && (
<Text title={formattedMessage} ellipsis>
{formattedMessage}
</Text>
)}
{link && (
<a href={link.url} target="_blank" rel="noopener noreferrer">
<Padded left size="xs">
<Flex alignItems="center">
<Text
style={{ maxWidth: '120px' }}
ellipsis
fontWeight="bold"
color="blue"
title={formatMessage(link.label)}
>
{formatMessage(link.label)}
</Text>
<Padded left size="xs" />
<LinkArrow />
</Flex>
</Padded>
</a>
)}
</Flex>
</Padded>
<RemoveWrapper>
<Remove onClick={handleClose} />
</RemoveWrapper>
</Flex>
</Padded>
</NotificationWrapper>
);
};
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',
},
};
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} />
);
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>
</>
);
}
}
Notification.defaultProps = {
notification: {
id: 1,
type: 'success',
message: {
id: 'notification.success.saved',
defaultMessage: 'Saved',
},
message: 'app.utils.defaultMessage',
status: 'success',
},
onClose: () => null,
};
Notification.propTypes = {
@ -114,27 +92,12 @@ Notification.propTypes = {
message: PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string,
id: PropTypes.string,
values: PropTypes.object,
}),
]),
title: PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string,
values: PropTypes.object,
}),
link: PropTypes.shape({
url: PropTypes.string.isRequired,
label: PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string,
values: PropTypes.object,
}).isRequired,
}),
type: PropTypes.string,
status: PropTypes.string,
}),
onClose: PropTypes.func,
onHideNotification: PropTypes.func.isRequired,
};

View File

@ -9,29 +9,33 @@ import PropTypes from 'prop-types';
import { CSSTransition } from 'react-transition-group';
import Notification from '../Notification';
import NotificationsWrapper from './NotificationsWrapper';
import Wrapper from './Wrapper';
/* eslint-disable */
const NotificationsContainer = ({ notifications, onHideNotification }) => {
if (notifications.length === 0) {
return null;
return false;
}
return (
<NotificationsWrapper>
{notifications.map(notification => (
<CSSTransition
key={notification.id}
classNames="notification"
timeout={{
enter: 500,
exit: 300,
}}
>
<Notification notification={notification} onHideNotification={onHideNotification} />
</CSSTransition>
))}
</NotificationsWrapper>
);
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 = {
@ -39,9 +43,7 @@ NotificationsContainer.defaultProps = {
{
id: 1,
message: 'app.utils.defaultMessage',
title: null,
link: null,
type: 'success',
status: 'success',
},
],
};

View File

@ -27,6 +27,7 @@ import PrivateRoute from '../PrivateRoute';
import Theme from '../Theme';
import { Content, Wrapper } from './components';
import { getDataSucceeded } from './actions';
import NewNotification from '../NewNotification';
function App(props) {
const getDataRef = useRef();
@ -97,6 +98,7 @@ function App(props) {
<Wrapper>
<GlobalStyle />
<NotificationProvider />
<NewNotification />
<Content>
<Switch>
<Route

View File

@ -82,7 +82,7 @@ const HomePage = ({ global: { strapiVersion }, history: { push } }) => {
id: 'notification.version.update.link',
},
},
timeout: 100000,
blockTransition: true,
onClose: () => localStorage.setItem('STRAPI_UPDATE_NOTIF', true),
});
}

View File

@ -0,0 +1,158 @@
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 } = notification;
const formattedMessage = formatMessage(typeof message === 'string' ? { id: message } : message);
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);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [blockTransition]);
return (
<NotificationWrapper 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={formatMessage(title)}
>
{formatMessage(title)}
</Text>
)}
<Flex>
{message && (
<Text title={formattedMessage} ellipsis>
{formattedMessage}
</Text>
)}
{link && (
<a href={link.url} target="_blank" rel="noopener noreferrer">
<Padded left size="xs">
<Flex alignItems="center">
<Text
style={{ maxWidth: '120px' }}
ellipsis
fontWeight="bold"
color="blue"
title={formatMessage(link.label)}
>
{formatMessage(link.label)}
</Text>
<Padded left size="xs" />
<LinkArrow />
</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,
},
};
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.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string,
values: PropTypes.object,
}),
link: PropTypes.shape({
url: PropTypes.string.isRequired,
label: 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,
}),
};
export default Notification;

View File

@ -43,7 +43,6 @@ const RemoveWrapper = styled.div`
position: relative;
display: flex;
width: 20px;
margin-right: 15px;
cursor: pointer;
opacity: 0.6;
font-size: 1.4rem;

View File

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

View File

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

View File

@ -0,0 +1,39 @@
/**
*
* NotificationsContainer
*
*/
import React 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.get('newNotification').notifications);
if (notifications.length === 0) {
return null;
}
return (
<NotificationsWrapper>
{notifications.map(notification => (
<CSSTransition
key={notification.id}
classNames="notification"
timeout={{
enter: 500,
exit: 300,
}}
>
<Notification notification={notification} />
</CSSTransition>
))}
</NotificationsWrapper>
);
};
export default NotificationsContainer;

View File

@ -0,0 +1,46 @@
/*
*
* NotificationProvider reducer
*
*/
import produce from 'immer';
import { get } from 'lodash';
import { SHOW_NEW_NOTIFICATION, HIDE_NEW_NOTIFICATION } from './constants';
const initialState = {
notifications: [],
};
const notificationReducer = (state = initialState, action) =>
// eslint-disable-next-line consistent-return
produce(state, draftState => {
switch (action.type) {
case SHOW_NEW_NOTIFICATION: {
draftState.notifications.push({
...action.config,
id: action.id,
type: get(action, ['config', 'type'], 'success'),
message: get(action, ['config', 'message'], {
id: 'notification.success.saved',
defaultMessage: 'Saved',
}),
});
break;
}
case HIDE_NEW_NOTIFICATION: {
const indexToRemove = state.notifications.findIndex(notif => notif.id === action.id);
if (indexToRemove !== -1) {
draftState.notifications.splice(indexToRemove, 1);
}
break;
}
default: {
return draftState;
}
}
});
export default notificationReducer;

View File

@ -0,0 +1,69 @@
import reducer from '../reducer';
import { SHOW_NEW_NOTIFICATION, HIDE_NEW_NOTIFICATION } from '../constants';
describe('AMDIN | CONTAINERS | NEWNOTIFICATION | reducer', () => {
describe('DEFAULT_ACTION', () => {
it('should return the initialState', () => {
const state = {
test: true,
};
expect(reducer(state, {})).toEqual(state);
});
});
describe('SHOW_NEW_NOTIFICATION', () => {
it('should add a notification', () => {
const action = {
type: SHOW_NEW_NOTIFICATION,
id: 1,
config: {
type: 'success',
message: {
id: 'notification.message',
},
},
};
const initialState = {
notifications: [],
};
const expected = {
notifications: [{ id: 1, message: { id: 'notification.message' }, type: 'success' }],
};
expect(reducer(initialState, action)).toEqual(expected);
});
});
describe('HIDE_NEW_NOTIFICATION', () => {
it('should remove a notification if the notification exist', () => {
const action = {
type: HIDE_NEW_NOTIFICATION,
id: 1,
};
const initialState = {
notifications: [{ id: 1, message: { id: 'notification.message' }, type: 'success' }],
};
const expected = {
notifications: [],
};
expect(reducer(initialState, action)).toEqual(expected);
});
it('should not remove the notification if the notification does not exist', () => {
const action = {
type: HIDE_NEW_NOTIFICATION,
id: 3,
};
const initialState = {
notifications: [{ id: 1, message: { id: 'notification.message' }, type: 'success' }],
};
const expected = {
notifications: [{ id: 1, message: { id: 'notification.message' }, type: 'success' }],
};
expect(reducer(initialState, action)).toEqual(expected);
});
});
});

View File

@ -7,40 +7,25 @@
/* eslint-disable import/no-cycle */
import { dispatch } from '../../app';
import { SHOW_NOTIFICATION, HIDE_NOTIFICATION, SHOW_NEW_NOTIFICATION } from './constants';
import { SHOW_NOTIFICATION, HIDE_NOTIFICATION } from './constants';
let nextNotificationId = 0;
const show = (config = {}) => {
export function showNotification(message, status) {
nextNotificationId++; // eslint-disable-line no-plusplus
// Start timeout to hide the notification
(id => {
setTimeout(() => {
dispatch(hideNotification(id));
}, config.timeout || 2500);
}, 250000);
})(nextNotificationId);
};
// TODO : To remove when the old notification api will be deleted from the codebase
export function showNotification(message, status) {
show();
return {
id: nextNotificationId,
type: SHOW_NOTIFICATION,
message,
status,
};
}
export function showNewNotification(config) {
show(config);
return {
id: nextNotificationId,
type: SHOW_NEW_NOTIFICATION,
config,
};
}

View File

@ -6,4 +6,3 @@
export const SHOW_NOTIFICATION = 'app/NotificationProvider/SHOW_NOTIFICATION';
export const HIDE_NOTIFICATION = 'app/NotificationProvider/HIDE_NOTIFICATION';
export const SHOW_NEW_NOTIFICATION = 'app/NotificatoinProvider/SHOW_NEW_NOTIFICATION';

View File

@ -25,7 +25,7 @@ export class NotificationProvider extends React.Component {
}
NotificationProvider.propTypes = {
notifications: PropTypes.array.isRequired,
notifications: PropTypes.object.isRequired,
onHideNotification: PropTypes.func.isRequired,
};

View File

@ -1,37 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
const NotificationComponent = () => {
const notificationInstances = document.querySelectorAll('*[id^="strapi-notif"]');
return (
<div
style={{
position: 'absolute',
top: (notificationInstances.length + 1) * 50,
left: 10,
backgroundColor: 'red',
color: 'black',
padding: '10px',
border: '1px solid',
}}
>
My custom notification
</div>
);
};
const displayNotif = () => {
const div = document.createElement('div');
div.setAttribute('id', 'strapi-notif');
document.body.appendChild(div);
ReactDOM.render(<NotificationComponent />, div);
setTimeout(() => {
document.body.removeChild(div);
}, 3000);
};
export default displayNotif;

View File

@ -4,52 +4,45 @@
*
*/
import produce from 'immer';
import { get } from 'lodash';
import { SHOW_NOTIFICATION, SHOW_NEW_NOTIFICATION, HIDE_NOTIFICATION } from './constants';
import { fromJS } from 'immutable';
import { SHOW_NOTIFICATION, HIDE_NOTIFICATION } from './constants';
const initialState = {
const initialState = fromJS({
notifications: [],
};
});
const notificationReducer = (state = initialState, action) =>
// eslint-disable-next-line consistent-return
produce(state, draftState => {
console.log(state);
switch (action.type) {
case SHOW_NEW_NOTIFICATION: {
draftState.notifications.push({
...action.config,
id: action.id,
type: get(action, ['config', 'type'], 'success'),
message: get(action, ['config', 'message'], {
id: 'notification.success.saved',
defaultMessage: 'Saved',
}),
});
break;
}
case SHOW_NOTIFICATION: {
draftState.notifications.push({
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',
type: action.status || 'success',
status: action.status || 'success',
id: action.id,
});
break;
}
case HIDE_NOTIFICATION: {
const indexToRemove = state.notifications.findIndex(notif => notif.id === action.id);
if (indexToRemove !== -1) {
draftState.notifications.splice(indexToRemove, 1);
})
);
case HIDE_NOTIFICATION:
// Check that the index exists
state.get('notifications').forEach((notification, i) => {
if (notification.id === action.id) {
index = i;
}
break;
});
if (typeof index !== 'undefined') {
// Remove the notification
return state.set('notifications', state.get('notifications').splice(index, 1));
}
default: {
return draftState;
}
}
});
// Notification not found, return the current state
return state;
default:
return state;
}
}
export default notificationReducer;
export default notificationProviderReducer;

View File

@ -14,15 +14,13 @@ const selectNotificationProviderDomain = () => state => state.get('notification'
*/
const selectNotificationProvider = () =>
createSelector(
selectNotificationProviderDomain(),
notificationProviderState => notificationProviderState
createSelector(selectNotificationProviderDomain(), notificationProviderState =>
notificationProviderState.toJS()
);
const selectNotifications = () =>
createSelector(
selectNotificationProviderDomain(),
notificationProviderState => notificationProviderState.notifications
createSelector(selectNotificationProviderDomain(), notificationProviderState =>
notificationProviderState.get('notifications')
);
export default selectNotificationProvider;

View File

@ -8,6 +8,7 @@ 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';
/**
* Creates the main reducer with the dynamically injected ones
@ -18,6 +19,7 @@ export default function createReducer(injectedReducers) {
admin: adminReducer,
language: languageProviderReducer,
notification: notificationProviderReducer,
newNotification: newNotificationReducer,
...injectedReducers,
});
}