WIP Refactor Notifications
Most of the pieces are there, still figuring out how the automatic dismissing is going to be handledpull/10616/head
parent
59cd86dedb
commit
93379ed23e
|
@ -3,7 +3,6 @@ import reducer, {initialState} from 'src/shared/reducers/notifications'
|
|||
import {
|
||||
publishNotification,
|
||||
dismissNotification,
|
||||
dismissAllNotifications,
|
||||
} from 'src/shared/actions/notifications'
|
||||
|
||||
const notificationID = '000'
|
||||
|
@ -12,11 +11,14 @@ const exampleNotification = {
|
|||
id: notificationID,
|
||||
type: 'success',
|
||||
message: 'Hell yeah you are a real notification!',
|
||||
duration: 5000,
|
||||
created: 'timestamp',
|
||||
duration: 5000, // -1 stays until dismissed
|
||||
icon: 'zap',
|
||||
}
|
||||
|
||||
describe('fsd', () => {
|
||||
const exampleNotifications = [exampleNotification]
|
||||
|
||||
describe('Shared.Reducers.notifications', () => {
|
||||
it('should publish a notification', () => {
|
||||
const actual = reducer(
|
||||
initialState,
|
||||
|
@ -28,15 +30,11 @@ describe('fsd', () => {
|
|||
})
|
||||
|
||||
it('should dismiss a notification', () => {
|
||||
const actual = reducer(initialState, dismissNotification(notificationID))
|
||||
const expected = initialState.filter(n => n.id !== notificationID)
|
||||
|
||||
expect(actual).to.equal(expected)
|
||||
})
|
||||
|
||||
it('should dismiss all notifications', () => {
|
||||
const actual = reducer(initialState, dismissAllNotifications())
|
||||
const expected = initialState
|
||||
const actual = reducer(
|
||||
exampleNotifications,
|
||||
dismissNotification(notificationID)
|
||||
)
|
||||
const expected = exampleNotifications.filter(n => n.id !== notificationID)
|
||||
|
||||
expect(actual).to.equal(expected)
|
||||
})
|
||||
|
|
|
@ -1,31 +1,17 @@
|
|||
export function publishNotification(type, message, options = {once: false}) {
|
||||
// this validator is purely for development purposes. It might make sense to move this to a middleware.
|
||||
const validTypes = ['error', 'success', 'warning', 'info']
|
||||
if (!validTypes.includes(type) || message === undefined) {
|
||||
console.error('handleNotification must have a valid type and text') // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
export function publishNotification(notification) {
|
||||
return {
|
||||
type: 'NOTIFICATION_RECEIVED',
|
||||
type: 'PUBLISH_NOTIFICATION',
|
||||
payload: {
|
||||
type,
|
||||
message,
|
||||
once: options.once,
|
||||
notification,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function dismissNotification(type) {
|
||||
export function dismissNotification(notificationID) {
|
||||
return {
|
||||
type: 'NOTIFICATION_DISMISSED',
|
||||
payload: {
|
||||
type,
|
||||
notificationID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function dismissAllNotifications() {
|
||||
return {
|
||||
type: 'ALL_NOTIFICATIONS_DISMISSED',
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
|
||||
import {dismissNotification as dismissNotificationAction} from 'shared/actions/notifications'
|
||||
|
||||
class Notification extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
getOpacity = () => {
|
||||
const {notification: {created, duration}} = this.props
|
||||
const expirationTime = created + duration
|
||||
|
||||
if (duration === -1) {
|
||||
// Notification is present until user dismisses it
|
||||
return false
|
||||
}
|
||||
|
||||
if (expirationTime < Date.now()) {
|
||||
return true
|
||||
// dismissNotification(id)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
notification: {id, type, message, icon},
|
||||
dismissNotification,
|
||||
} = this.props
|
||||
|
||||
const notificationClass = `alert alert-${type}`
|
||||
|
||||
const notificationStyle = {
|
||||
opacity: this.getOpacity(),
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={notificationClass}>
|
||||
{icon && <span className={`icon ${icon}`} />}
|
||||
<div className="alert-message">
|
||||
{message}
|
||||
</div>
|
||||
<button className="alert-close" onClick={dismissNotification(id)}>
|
||||
<span className="icon remove" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {func, number, shape, string} = PropTypes
|
||||
|
||||
Notification.propTypes = {
|
||||
notification: shape({
|
||||
id: string.isRequired,
|
||||
type: string.isRequired,
|
||||
message: string.isRequired,
|
||||
created: number.isRequired,
|
||||
duration: number.isRequired,
|
||||
icon: string,
|
||||
}),
|
||||
dismissNotification: func.isRequired,
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
dismissNotification: bindActionCreators(dismissNotificationAction, dispatch),
|
||||
})
|
||||
|
||||
export default connect(null, mapDispatchToProps)(Notification)
|
|
@ -1,106 +1,31 @@
|
|||
import React, {Component} from 'react'
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import {withRouter} from 'react-router'
|
||||
import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
|
||||
import {getNotificationID} from 'src/shared/reducers/notifications'
|
||||
import Notification from 'src/shared/components/Notification'
|
||||
|
||||
import {
|
||||
publishNotification as publishNotificationAction,
|
||||
dismissNotification as dismissNotificationAction,
|
||||
dismissAllNotifications as dismissAllNotificationsAction,
|
||||
} from 'shared/actions/notifications'
|
||||
const Notifications = ({notifications}) =>
|
||||
<div className="flash-messages">
|
||||
{notifications.map(n => <Notification key={n.id} notification={n} />)}
|
||||
</div>
|
||||
|
||||
class Notifications extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.location.pathname !== this.props.location.pathname) {
|
||||
this.props.dismissAllNotifications()
|
||||
}
|
||||
}
|
||||
|
||||
renderNotification = (type, message) => {
|
||||
const isDismissed = this.props.dismissedNotifications[
|
||||
getNotificationID(message, type)
|
||||
]
|
||||
if (!message || isDismissed) {
|
||||
return null
|
||||
}
|
||||
const alertClassname = classnames('alert', {
|
||||
'alert-danger': type === 'error',
|
||||
'alert-success': type === 'success',
|
||||
'alert-warning': type === 'warning',
|
||||
})
|
||||
return (
|
||||
<div className={alertClassname}>
|
||||
{message}
|
||||
{this.renderDismiss(type)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
handleDismiss = type => () => this.props.dismissNotification(type)
|
||||
|
||||
renderDismiss = type => {
|
||||
return (
|
||||
<button className="alert-close" onClick={this.handleDismiss(type)}>
|
||||
<span className="icon remove" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {success, error, warning} = this.props.notifications
|
||||
if (!success && !error && !warning) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flash-messages">
|
||||
{this.renderNotification('success', success)}
|
||||
{this.renderNotification('error', error)}
|
||||
{this.renderNotification('warning', warning)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
const {arrayOf, number, shape, string} = PropTypes
|
||||
|
||||
Notifications.propTypes = {
|
||||
location: shape({
|
||||
pathname: string.isRequired,
|
||||
}).isRequired,
|
||||
publishNotification: func.isRequired,
|
||||
dismissNotification: func.isRequired,
|
||||
dismissAllNotifications: func.isRequired,
|
||||
notifications: shape({
|
||||
success: string,
|
||||
error: string,
|
||||
warning: string,
|
||||
notifications: arrayOf({
|
||||
notification: shape({
|
||||
id: string.isRequired,
|
||||
type: string.isRequired,
|
||||
message: string.isRequired,
|
||||
created: number.isRequired,
|
||||
duration: number.isRequired,
|
||||
icon: string,
|
||||
}),
|
||||
}),
|
||||
dismissedNotifications: shape({}),
|
||||
}
|
||||
|
||||
const mapStateToProps = ({notifications, dismissedNotifications}) => ({
|
||||
const mapStateToProps = ({notifications}) => ({
|
||||
notifications,
|
||||
dismissedNotifications,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
publishNotification: bindActionCreators(publishNotificationAction, dispatch),
|
||||
dismissNotification: bindActionCreators(dismissNotificationAction, dispatch),
|
||||
dismissAllNotifications: bindActionCreators(
|
||||
dismissAllNotificationsAction,
|
||||
dispatch
|
||||
),
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(
|
||||
withRouter(Notifications)
|
||||
)
|
||||
export default connect(mapStateToProps, null)(Notifications)
|
||||
|
|
|
@ -445,3 +445,5 @@ export const cellSupportsAnnotations = cellType => {
|
|||
]
|
||||
return !!supportedTypes.find(type => type === cellType)
|
||||
}
|
||||
|
||||
export const NOTIFICATION_FADE_DURATION = 1000
|
||||
|
|
|
@ -1,49 +1,21 @@
|
|||
import u from 'updeep'
|
||||
import _ from 'lodash'
|
||||
import uuid from 'uuid'
|
||||
|
||||
export const initialState = []
|
||||
|
||||
export const notifications = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case 'NOTIFICATION_RECEIVED': {
|
||||
const {type, message} = action.payload
|
||||
return u.updateIn(type, message, state)
|
||||
}
|
||||
case 'NOTIFICATION_DISMISSED': {
|
||||
const {type} = action.payload
|
||||
return u(u.omit(type), state)
|
||||
}
|
||||
case 'ALL_NOTIFICATIONS_DISMISSED': {
|
||||
// Reset to initial state
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
export const getNotificationID = (message, type) => _.snakeCase(message) + type
|
||||
|
||||
export const dismissedNotifications = (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case 'NOTIFICATION_RECEIVED': {
|
||||
const {type, message, once} = action.payload
|
||||
if (once) {
|
||||
// Create a message ID in a deterministic way, also with its type
|
||||
const messageID = getNotificationID(message, type)
|
||||
if (state[messageID]) {
|
||||
// Message action called with once option but we've already seen it
|
||||
return state
|
||||
}
|
||||
// Message action called with once option and it's not present on
|
||||
// the persisted state
|
||||
return {
|
||||
...state,
|
||||
[messageID]: true,
|
||||
}
|
||||
case 'PUBLISH_NOTIFICATION': {
|
||||
const newNotification = {
|
||||
...action.payload,
|
||||
id: uuid.v4(),
|
||||
created: Date.now(),
|
||||
}
|
||||
// Message action not called with once option
|
||||
return state
|
||||
|
||||
return [...state, newNotification]
|
||||
}
|
||||
|
||||
case 'DISMISS_NOTIFICATION': {
|
||||
return state.filter(n => n.id === action.payload)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue