WIP Refactor Notifications

Most of the pieces are there, still figuring out how the automatic
dismissing is going to be handled
pull/10616/head
Alex P 2018-03-01 19:31:19 -08:00 committed by Andrew Watkins
parent 59cd86dedb
commit 93379ed23e
6 changed files with 118 additions and 164 deletions

View File

@ -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)
})

View File

@ -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',
}
}

View File

@ -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)

View File

@ -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)

View File

@ -445,3 +445,5 @@ export const cellSupportsAnnotations = cellType => {
]
return !!supportedTypes.find(type => type === cellType)
}
export const NOTIFICATION_FADE_DURATION = 1000

View File

@ -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)
}
}