From 6152fb7706f044199cf812644a4125410c74c3b4 Mon Sep 17 00:00:00 2001 From: Ben Wheeler <wheeler.benjamin@gmail.com> Date: Wed, 14 Nov 2018 15:14:18 -0500 Subject: [PATCH] menu bar save messaging; self-dismissing alerts --- package.json | 1 + src/components/alerts/alert.jsx | 28 +++++----- src/components/alerts/alerts.jsx | 1 + src/components/alerts/inline-message.css | 27 ++++++++++ src/components/alerts/inline-message.jsx | 39 ++++++++++++++ src/components/menu-bar/menu-bar.jsx | 4 ++ src/components/spinner/spinner.css | 12 ++++- src/components/spinner/spinner.jsx | 9 ++-- src/containers/alert.jsx | 3 ++ src/containers/alerts.jsx | 7 ++- src/containers/inline-messages.jsx | 55 ++++++++++++++++++++ src/css/colors.css | 2 + src/lib/alerts/index.jsx | 29 ++++++++--- src/lib/app-state-hoc.jsx | 12 +++-- src/lib/project-saver-hoc.jsx | 21 ++++---- src/reducers/alerts.js | 63 ++++++++++++++++++++--- test/unit/reducers/alerts-reducer.test.js | 23 +++++++-- 17 files changed, 288 insertions(+), 48 deletions(-) create mode 100644 src/components/alerts/inline-message.css create mode 100644 src/components/alerts/inline-message.jsx create mode 100644 src/containers/inline-messages.jsx diff --git a/package.json b/package.json index aa891d5be..5334feda6 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "react-tooltip": "3.8.0", "react-virtualized": "9.20.1", "redux": "3.7.2", + "redux-thunk": "2.0.1", "redux-mock-store": "^1.2.3", "redux-throttle": "0.1.1", "rimraf": "^2.6.1", diff --git a/src/components/alerts/alert.jsx b/src/components/alerts/alert.jsx index 89d7f6b90..04ea672c4 100644 --- a/src/components/alerts/alert.jsx +++ b/src/components/alerts/alert.jsx @@ -17,6 +17,7 @@ const closeButtonColors = { const AlertComponent = ({ content, + closeButton, iconSpinner, iconURL, level, @@ -44,7 +45,7 @@ const AlertComponent = ({ {content} </div> </div> - {showReconnect ? ( + {showReconnect && ( <button className={styles.connectionButton} onClick={onReconnect} @@ -55,21 +56,24 @@ const AlertComponent = ({ id="gui.connection.reconnect" /> </button> - ) : null} - <Box - className={styles.alertCloseButtonContainer} - > - <CloseButton - className={classNames(styles.alertCloseButton)} - color={closeButtonColors[level]} - size={CloseButton.SIZE_LARGE} - onClick={onCloseAlert} - /> - </Box> + )} + {closeButton && ( + <Box + className={styles.alertCloseButtonContainer} + > + <CloseButton + className={classNames(styles.alertCloseButton)} + color={closeButtonColors[level]} + size={CloseButton.SIZE_LARGE} + onClick={onCloseAlert} + /> + </Box> + )} </Box> ); AlertComponent.propTypes = { + closeButton: PropTypes.bool, content: PropTypes.element, iconSpinner: PropTypes.bool, iconURL: PropTypes.string, diff --git a/src/components/alerts/alerts.jsx b/src/components/alerts/alerts.jsx index 115f28c9f..0ae89dfb0 100644 --- a/src/components/alerts/alerts.jsx +++ b/src/components/alerts/alerts.jsx @@ -15,6 +15,7 @@ const AlertsComponent = ({ > {alertsList.map((a, index) => ( <Alert + closeButton={a.closeButton} content={a.content} extensionId={a.extensionId} iconSpinner={a.iconSpinner} diff --git a/src/components/alerts/inline-message.css b/src/components/alerts/inline-message.css new file mode 100644 index 000000000..07bba4e4e --- /dev/null +++ b/src/components/alerts/inline-message.css @@ -0,0 +1,27 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; + +.inline-message { + color: $ui-white; + font-family: "Helvetica Neue"; + display: flex; + justify-content: end; + align-items: center; + font-size: .8125rem; +} + +.success { + color: $ui-white-dim; +} + +.info { + color: $ui-white; +} + +.warn { + color: $error-light; +} + +.spinner { + margin-right: $space; +} diff --git a/src/components/alerts/inline-message.jsx b/src/components/alerts/inline-message.jsx new file mode 100644 index 000000000..8a7330f79 --- /dev/null +++ b/src/components/alerts/inline-message.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import Spinner from '../spinner/spinner.jsx'; +import {AlertLevels} from '../../lib/alerts/index.jsx'; + +import styles from './inline-message.css'; + +const InlineMessageComponent = ({ + content, + iconSpinner, + level +}) => ( + <div + className={classNames(styles.inlineMessage, styles[level])} + > + {/* TODO: implement Rtl handling */} + {iconSpinner && ( + <Spinner + small + className={styles.spinner} + /> + )} + {content} + </div> +); + +InlineMessageComponent.propTypes = { + content: PropTypes.element, + iconSpinner: PropTypes.bool, + level: PropTypes.string +}; + +InlineMessageComponent.defaultProps = { + level: AlertLevels.INFO +}; + +export default InlineMessageComponent; diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index 12fe0c273..1102e619b 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -12,6 +12,7 @@ import ShareButton from './share-button.jsx'; import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx'; import Divider from '../divider/divider.jsx'; import LanguageSelector from '../../containers/language-selector.jsx'; +import InlineMessages from '../../containers/inline-messages.jsx'; import SBFileUploader from '../../containers/sb-file-uploader.jsx'; import ProjectWatcher from '../../containers/project-watcher.jsx'; import MenuBarMenu from './menu-bar-menu.jsx'; @@ -531,6 +532,9 @@ class MenuBar extends React.Component { {/* show the proper UI in the account menu, given whether the user is logged in, and whether a session is available to log in with */} <div className={styles.accountInfoGroup}> + <div className={styles.menuBarItem}> + <InlineMessages /> + </div> {this.props.sessionExists ? ( this.props.username ? ( // ************ user is logged in ************ diff --git a/src/components/spinner/spinner.css b/src/components/spinner/spinner.css index 753cb2056..a20e79b13 100644 --- a/src/components/spinner/spinner.css +++ b/src/components/spinner/spinner.css @@ -11,7 +11,7 @@ border-color: $ui-white-transparent; } -.spinner::after, .spinner::before { +.spinner::before, .spinner::after { width: 1rem; height: 1rem; content: ''; @@ -28,6 +28,16 @@ animation: spin 1.5s cubic-bezier(0.4, 0.1, 0.4, 1) infinite; } +.small { + width: .5rem; + height: .5rem; +} + +.small::before, .small::after { + width: .5rem; + height: .5rem; +} + @keyframes spin { 0% { transform: rotate(0deg); diff --git a/src/components/spinner/spinner.jsx b/src/components/spinner/spinner.jsx index 08685ccb3..bb82fbf51 100644 --- a/src/components/spinner/spinner.jsx +++ b/src/components/spinner/spinner.jsx @@ -6,19 +6,22 @@ import styles from './spinner.css'; const SpinnerComponent = function (props) { const { - className + className, + small } = props; return ( <div className={classNames( + className, styles.spinner, - className + {[styles.small]: small} )} /> ); }; SpinnerComponent.propTypes = { - className: PropTypes.string + className: PropTypes.string, + small: PropTypes.bool }; SpinnerComponent.defaultProps = { className: '' diff --git a/src/containers/alert.jsx b/src/containers/alert.jsx index f22cf06fb..9235d8d8a 100644 --- a/src/containers/alert.jsx +++ b/src/containers/alert.jsx @@ -24,6 +24,7 @@ class Alert extends React.Component { } render () { const { + closeButton, content, index, // eslint-disable-line no-unused-vars level, @@ -34,6 +35,7 @@ class Alert extends React.Component { } = this.props; return ( <AlertComponent + closeButton={closeButton} content={content} iconSpinner={iconSpinner} iconURL={iconURL} @@ -57,6 +59,7 @@ const mapDispatchToProps = dispatch => ({ }); Alert.propTypes = { + closeButton: PropTypes.bool, content: PropTypes.element, extensionId: PropTypes.string, iconSpinner: PropTypes.bool, diff --git a/src/containers/alerts.jsx b/src/containers/alerts.jsx index 21a17700a..5ac3f0a7a 100644 --- a/src/containers/alerts.jsx +++ b/src/containers/alerts.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; +import {AlertTypes} from '../lib/alerts/index.jsx'; import { closeAlert @@ -14,7 +15,11 @@ const Alerts = ({ onCloseAlert }) => ( <AlertsComponent - alertsList={alertsList} + // only display standard and extension alerts here + alertsList={alertsList.filter(curAlert => ( + curAlert.alertType === AlertTypes.STANDARD || + curAlert.alertType === AlertTypes.EXTENSION + ))} className={className} onCloseAlert={onCloseAlert} /> diff --git a/src/containers/inline-messages.jsx b/src/containers/inline-messages.jsx new file mode 100644 index 000000000..a55bc8997 --- /dev/null +++ b/src/containers/inline-messages.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {connect} from 'react-redux'; +import {AlertTypes} from '../lib/alerts/index.jsx'; + +import InlineMessageComponent from '../components/alerts/inline-message.jsx'; + +const InlineMessages = ({ + alertsList, + className +}) => { + if (!alertsList) { + return null; + } + // only display inline alerts here + const inlineAlerts = alertsList.filter(curAlert => ( + curAlert.alertType === AlertTypes.INLINE + )); + if (!inlineAlerts || !inlineAlerts.length) { + return null; + } + + // get first alert + const firstInlineAlert = inlineAlerts[0]; + const { + content, + iconSpinner, + level + } = firstInlineAlert; + + return ( + <InlineMessageComponent + className={className} + content={content} + iconSpinner={iconSpinner} + level={level} + /> + ); +}; + +InlineMessages.propTypes = { + alertsList: PropTypes.arrayOf(PropTypes.object), + className: PropTypes.string +}; + +const mapStateToProps = state => ({ + alertsList: state.scratchGui.alerts.alertsList +}); + +const mapDispatchToProps = () => ({}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(InlineMessages); diff --git a/src/css/colors.css b/src/css/colors.css index fd930ed62..9caf8430d 100644 --- a/src/css/colors.css +++ b/src/css/colors.css @@ -5,6 +5,7 @@ $ui-tertiary: hsla(215, 50%, 90%, 1); /* #D9E3F2 */ $ui-modal-overlay: hsla(215, 100%, 65%, 0.9); /* 90% transparent version of motion-primary */ $ui-white: hsla(0, 100%, 100%, 1); /* #FFFFFF */ +$ui-white-dim: hsla(0, 100%, 100%, 0.75); /* 25% transparent version of ui-white */ $ui-white-transparent: hsla(0, 100%, 100%, 0.25); /* 25% transparent version of ui-white */ $ui-transparent: hsla(0, 100%, 100%, 0); /* 25% transparent version of ui-white */ @@ -32,6 +33,7 @@ $pen-primary: hsla(163, 85%, 40%, 1); /* #0FBD8C */ $pen-transparent: hsla(163, 85%, 40%, 0.25); /* #0FBD8C */ $error-primary: hsla(30, 100%, 55%, 1); /* #FF8C1A */ +$error-light: hsla(30, 100%, 70%, 1); /* #FFB366 */ $error-transparent: hsla(30, 100%, 55%, 0.25); /* #FF8C1A */ $extensions-primary: hsla(163, 85%, 40%, 1); /* #0FBD8C */ diff --git a/src/lib/alerts/index.jsx b/src/lib/alerts/index.jsx index bb4d60206..aec4134f2 100644 --- a/src/lib/alerts/index.jsx +++ b/src/lib/alerts/index.jsx @@ -1,17 +1,26 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; +import keyMirror from 'keymirror'; import successImage from '../assets/icon--success.svg'; +const AlertTypes = keyMirror({ + STANDARD: null, + EXTENSION: null, + INLINE: null +}); + const AlertLevels = { SUCCESS: 'success', + INFO: 'info', WARN: 'warn' }; const alerts = [ { alertId: 'createSuccess', - clearList: ['creating'], + alertType: AlertTypes.STANDARD, + clearList: ['createSuccess, creating, saveSuccess, saving'], content: ( <FormattedMessage defaultMessage="Successfully created." @@ -20,10 +29,13 @@ const alerts = [ /> ), iconURL: successImage, - level: AlertLevels.SUCCESS + level: AlertLevels.SUCCESS, + maxDisplaySecs: 5 }, { alertId: 'creating', + alertType: AlertTypes.STANDARD, + clearList: ['createSuccess, creating, saveSuccess, saving'], content: ( <FormattedMessage defaultMessage="Creating..." @@ -58,7 +70,8 @@ const alerts = [ }, { alertId: 'saveSuccess', - clearList: ['saving'], + alertType: AlertTypes.INLINE, + clearList: ['createSuccess, creating, saveSuccess, saving'], content: ( <FormattedMessage defaultMessage="Successfully saved." @@ -67,10 +80,13 @@ const alerts = [ /> ), iconURL: successImage, - level: AlertLevels.SUCCESS + level: AlertLevels.SUCCESS, + maxDisplaySecs: 5 }, { alertId: 'saving', + alertType: AlertTypes.INLINE, + clearList: ['createSuccess, creating, saveSuccess, saving'], content: ( <FormattedMessage defaultMessage="Saving..." @@ -79,11 +95,12 @@ const alerts = [ /> ), iconSpinner: true, - level: AlertLevels.SUCCESS + level: AlertLevels.INFO } ]; export { alerts as default, - AlertLevels + AlertLevels, + AlertTypes }; diff --git a/src/lib/app-state-hoc.jsx b/src/lib/app-state-hoc.jsx index 9177c5219..4439d277b 100644 --- a/src/lib/app-state-hoc.jsx +++ b/src/lib/app-state-hoc.jsx @@ -1,7 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import {Provider} from 'react-redux'; -import {createStore, combineReducers, compose} from 'redux'; +import {applyMiddleware, createStore, combineReducers, compose} from 'redux'; +import thunk from 'redux-thunk'; import ConnectedIntlProvider from './connected-intl-provider.jsx'; import localesReducer, {initLocale, localesInitialState} from '../reducers/locales'; @@ -45,7 +46,9 @@ const AppStateHOC = function (WrappedComponent, localesOnly) { // browser modal reducers = {locales: localesReducer}; initialState = {locales: initializedLocales}; - enhancer = composeEnhancers(); + enhancer = composeEnhancers( + applyMiddleware(thunk) + ); } else { // You are right, this is gross. But it's necessary to avoid // importing unneeded code that will crash unsupported browsers. @@ -85,7 +88,10 @@ const AppStateHOC = function (WrappedComponent, localesOnly) { locales: initializedLocales, scratchGui: initializedGui }; - enhancer = composeEnhancers(guiMiddleware); + enhancer = composeEnhancers( + guiMiddleware, + applyMiddleware(thunk) + ); } const reducer = combineReducers(reducers); this.store = createStore( diff --git a/src/lib/project-saver-hoc.jsx b/src/lib/project-saver-hoc.jsx index 631fc7798..9389d3c6d 100644 --- a/src/lib/project-saver-hoc.jsx +++ b/src/lib/project-saver-hoc.jsx @@ -5,7 +5,10 @@ import VM from 'scratch-vm'; import log from '../lib/log'; import storage from '../lib/storage'; -import {showStandardAlert} from '../reducers/alerts'; +import { + showAlertWithTimeout, + showStandardAlert +} from '../reducers/alerts'; import { LoadingStates, autoUpdateProject, @@ -73,14 +76,10 @@ const ProjectSaverHOC = function (WrappedComponent) { } } updateProjectToStorage () { - if (this.props.isManualUpdating) { - this.props.onShowSavingAlert(); - } + this.props.onShowSavingAlert(); return this.storeProject(this.props.reduxProjectId) .then(() => { - if (this.props.isManualUpdating) { - this.props.onShowSaveSuccessAlert(); - } + this.props.onShowSaveSuccessAlert(); // there is nothing we expect to find in response that we need to check here this.props.onUpdatedProject(this.props.loadingState); }) @@ -260,10 +259,10 @@ const ProjectSaverHOC = function (WrappedComponent) { onManualUpdateProject: () => dispatch(manualUpdateProject()), onProjectError: error => dispatch(projectError(error)), onShowAlert: alertType => dispatch(showStandardAlert(alertType)), - onShowCreateSuccessAlert: () => dispatch(showStandardAlert('createSuccess')), - onShowCreatingAlert: () => dispatch(showStandardAlert('creating')), - onShowSaveSuccessAlert: () => dispatch(showStandardAlert('saveSuccess')), - onShowSavingAlert: () => dispatch(showStandardAlert('saving')), + onShowCreateSuccessAlert: () => dispatch(showAlertWithTimeout('createSuccess')), + onShowCreatingAlert: () => dispatch(showAlertWithTimeout('creating')), + onShowSaveSuccessAlert: () => dispatch(showAlertWithTimeout('saveSuccess')), + onShowSavingAlert: () => dispatch(showAlertWithTimeout('saving')), onUpdatedProject: (projectId, loadingState) => dispatch(doneUpdatingProject(projectId, loadingState)) }); // Allow incoming props to override redux-provided props. Used to mock in tests. diff --git a/src/reducers/alerts.js b/src/reducers/alerts.js index d542c4037..793c47d39 100644 --- a/src/reducers/alerts.js +++ b/src/reducers/alerts.js @@ -1,26 +1,30 @@ import alertsData from '../lib/alerts/index.jsx'; -import {AlertLevels} from '../lib/alerts/index.jsx'; +import {AlertTypes, AlertLevels} from '../lib/alerts/index.jsx'; import extensionData from '../lib/libraries/extensions/index.jsx'; const SHOW_STANDARD_ALERT = 'scratch-gui/alerts/SHOW_STANDARD_ALERT'; const SHOW_EXTENSION_ALERT = 'scratch-gui/alerts/SHOW_EXTENSION_ALERT'; const CLOSE_ALERT = 'scratch-gui/alerts/CLOSE_ALERT'; +const CLOSE_ALERTS_WITH_ID = 'scratch-gui/alerts/CLOSE_ALERTS_WITH_ID'; const initialState = { visible: true, // list of alerts, each with properties: + // * alert type (required): one of AlertTypes + // * closeButton (optional): bool indicating that we should show close button // * content (optional): react element (a <FormattedMessage />) // * extentionId (optional): id string that identifies the extension // * iconURL (optional): string // * level (required): string, one of AlertLevels // * message (optional): string + // * showReconnect (optional): bool alertsList: [] }; const reducer = function (state, action) { if (typeof state === 'undefined') state = initialState; switch (action.type) { - case SHOW_STANDARD_ALERT: { + case SHOW_STANDARD_ALERT: { // also will show inline alerts const alertId = action.alertId; if (alertId) { const newAlert = { @@ -29,20 +33,21 @@ const reducer = function (state, action) { }; const alertData = alertsData.find(thisAlertData => thisAlertData.alertId === alertId); if (alertData) { - let newList = state.alertsList.slice(); - newList = newList.filter(curAlert => ( + const newList = state.alertsList.filter(curAlert => ( !alertData.clearList || alertData.clearList.indexOf(curAlert.alertId) )); if (action.data && action.data.message) { newAlert.message = action.data.message; } + newAlert.alertType = alertData.alertType || AlertTypes.STANDARD; + newAlert.closeButton = alertData.closeButton; newAlert.content = alertData.content; newAlert.iconURL = alertData.iconURL; newAlert.iconSpinner = alertData.iconSpinner; newAlert.level = alertData.level; - newList.push(newAlert); + newList.push(newAlert); return Object.assign({}, state, { alertsList: newList }); @@ -53,6 +58,7 @@ const reducer = function (state, action) { case SHOW_EXTENSION_ALERT: { const newList = state.alertsList.slice(); const newAlert = { + alertType: AlertTypes.EXTENSION, message: action.data.message, level: AlertLevels.WARN }; @@ -69,6 +75,7 @@ const reducer = function (state, action) { if (extension.smallPeripheralImage) { newAlert.iconURL = extension.smallPeripheralImage; } + newAlert.closeButton = true; } } newList.push(newAlert); @@ -83,13 +90,20 @@ const reducer = function (state, action) { alertsList: newList }); } + case CLOSE_ALERTS_WITH_ID: { + return Object.assign({}, state, { + alertsList: state.alertsList.filter(curAlert => ( + curAlert.alertId !== action.alertId + )) + }); + } default: return state; } }; /** - * Function to close an alert with the given index. + * Action creator to close an alert with the given index. * * @param {object} index - the index of the alert to close. * @return {object} - an object to be passed to the reducer. @@ -102,7 +116,20 @@ const closeAlert = function (index) { }; /** - * Function to show an alert with the given alertId. + * Action creator to close all alerts with a given ID. + * + * @param {string} alertId - id string of the alert to close + * @return {object} - an object to be passed to the reducer. + */ +const closeAlertsWithId = function (alertId) { + return { + type: CLOSE_ALERTS_WITH_ID, + alertId + }; +}; + +/** + * Action creator to show an alert with the given alertId. * * @param {string} alertId - id string of the alert to show * @return {object} - an object to be passed to the reducer. @@ -115,7 +142,7 @@ const showStandardAlert = function (alertId) { }; /** - * Function to show an alert with the given input data. + * Action creator to show an alert with the given input data. * * @param {object} data - data for the alert * @param {string} data.message - message for the alert @@ -129,10 +156,30 @@ const showExtensionAlert = function (data) { }; }; +/** + * Function to dispatch showing an alert, with optional + * timeout to make it close/go away. + * + * @param {object} alertId - the ID of the alert + * @return {null} - do not return a value + */ +const showAlertWithTimeout = alertId => (dispatch => { + const alertData = alertsData.find(thisAlertData => thisAlertData.alertId === alertId); + if (alertData) { + dispatch(showStandardAlert(alertId)); + if (alertData.maxDisplaySecs) { + setTimeout(() => { + dispatch(closeAlertsWithId(alertId)); + }, alertData.maxDisplaySecs * 1000); + } + } +}); + export { reducer as default, initialState as alertsInitialState, closeAlert, + showAlertWithTimeout, showExtensionAlert, showStandardAlert }; diff --git a/test/unit/reducers/alerts-reducer.test.js b/test/unit/reducers/alerts-reducer.test.js index b289813e3..21c1de015 100644 --- a/test/unit/reducers/alerts-reducer.test.js +++ b/test/unit/reducers/alerts-reducer.test.js @@ -1,7 +1,7 @@ // TODO: add tests of extension alerts /* eslint-env jest */ -import {AlertLevels} from '../../../src/lib/alerts/index.jsx'; +import {AlertTypes, AlertLevels} from '../../../src/lib/alerts/index.jsx'; import alertsReducer from '../../../src/reducers/alerts'; import { closeAlert, @@ -18,10 +18,11 @@ test('initialState', () => { test('create one standard alert', () => { let defaultState; - const action = showStandardAlert('saving'); + const action = showStandardAlert('creating'); const resultState = alertsReducer(defaultState, action); expect(resultState.alertsList.length).toBe(1); - expect(resultState.alertsList[0].alertId).toBe('saving'); + expect(resultState.alertsList[0].alertId).toBe('creating'); + expect(resultState.alertsList[0].alertType).toBe(AlertTypes.STANDARD); expect(resultState.alertsList[0].level).toBe(AlertLevels.SUCCESS); }); @@ -31,6 +32,7 @@ test('add several standard alerts', () => { alertsList: [ { alertId: 'saving', + alertType: AlertTypes.INLINE, level: AlertLevels.SUCCESS, content: null, iconURL: '/no_image_here.jpg' @@ -42,18 +44,31 @@ test('add several standard alerts', () => { resultState = alertsReducer(resultState, action); resultState = alertsReducer(resultState, action); expect(resultState.alertsList.length).toBe(4); + expect(resultState.alertsList[0].alertType).toBe(AlertTypes.INLINE); expect(resultState.alertsList[0].iconURL).toBe('/no_image_here.jpg'); + expect(resultState.alertsList[1].alertType).toBe(AlertTypes.STANDARD); expect(resultState.alertsList[1].alertId).toBe('creating'); expect(resultState.alertsList[2].alertId).toBe('creating'); expect(resultState.alertsList[3].alertId).toBe('creating'); }); +test('create one inline alert message', () => { + let defaultState; + const action = showStandardAlert('saving'); + const resultState = alertsReducer(defaultState, action); + expect(resultState.alertsList.length).toBe(1); + expect(resultState.alertsList[0].alertId).toBe('saving'); + expect(resultState.alertsList[0].alertType).toBe(AlertTypes.INLINE); + expect(resultState.alertsList[0].level).toBe(AlertLevels.INFO); +}); + test('can close alerts by index', () => { const initialState = { visible: true, alertsList: [ { alertId: 'saving', + alertType: AlertTypes.INLINE, level: AlertLevels.SUCCESS, content: null, iconURL: '/no_image_here.jpg' @@ -78,12 +93,14 @@ test('related alerts can clear each other', () => { alertsList: [ { alertId: 'saving', + alertType: AlertTypes.INLINE, level: AlertLevels.SUCCESS, content: null, iconURL: '/no_image_here.jpg' }, { alertId: 'creating', + alertType: AlertTypes.STANDARD, level: AlertLevels.SUCCESS, content: null, iconURL: '/no_image_here.jpg' -- GitLab