diff --git a/i18n/en.pot b/i18n/en.pot index 4b42c3961..d1adc2c0e 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,12 +5,15 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2020-09-08T17:54:39.674Z\n" -"PO-Revision-Date: 2020-09-08T17:54:39.674Z\n" +"POT-Creation-Date: 2020-10-12T06:11:46.298Z\n" +"PO-Revision-Date: 2020-10-12T06:11:46.298Z\n" msgid "Untitled dashboard" msgstr "" +msgid "Loading dashboard – {{name}}" +msgstr "" + msgid "Cancel" msgstr "" diff --git a/src/actions/alert.js b/src/actions/alert.js new file mode 100644 index 000000000..f92170988 --- /dev/null +++ b/src/actions/alert.js @@ -0,0 +1,10 @@ +import { SET_ALERT_MESSAGE, CLEAR_ALERT_MESSAGE } from '../reducers/alert' + +export const acSetAlertMessage = value => ({ + type: SET_ALERT_MESSAGE, + value, +}) + +export const acClearAlertMessage = () => ({ + type: CLEAR_ALERT_MESSAGE, +}) diff --git a/src/actions/selected.js b/src/actions/selected.js index 253eac62c..ffaa13b26 100644 --- a/src/actions/selected.js +++ b/src/actions/selected.js @@ -1,8 +1,5 @@ -import { - getCustomDashboards, - sGetDashboardById, - EMPTY_DASHBOARD, -} from '../reducers/dashboards' +import i18n from '@dhis2/d2-i18n' +import { getCustomDashboards, sGetDashboardById } from '../reducers/dashboards' import { SET_SELECTED_ID, SET_SELECTED_ISLOADING, @@ -17,9 +14,8 @@ import { sGetUserUsername } from '../reducers/user' import { acSetDashboardItems, acAppendDashboards } from './dashboards' import { acClearItemFilters } from './itemFilters' import { tGetMessages } from '../components/Item/MessagesItem/actions' -import { acReceivedSnackbarMessage, acCloseSnackbar } from './snackbar' +import { acSetAlertMessage, acClearAlertMessage } from './alert' import { acAddVisualization, acClearVisualizations } from './visualizations' - import { apiFetchDashboard } from '../api/dashboards' import { storePreferredDashboardId } from '../api/localStorage' import { @@ -28,7 +24,6 @@ import { } from '../api/description' import { withShape } from '../components/ItemGrid/gridUtil' -import { loadingDashboardMsg } from '../components/SnackbarMessage/SnackbarMessage' import { extractFavorite } from '../components/Item/VisualizationItem/plugin' import { @@ -75,18 +70,14 @@ export const acClearSelectedItemActiveTypes = () => ({ export const tSetSelectedDashboardById = id => async (dispatch, getState) => { dispatch(acSetSelectedIsLoading(true)) - const snackbarTimeout = setTimeout(() => { - const dashboardName = ( - sGetDashboardById(getState(), id) || EMPTY_DASHBOARD - ).displayName - if (sGetSelectedIsLoading(getState()) && dashboardName) { - loadingDashboardMsg.name = dashboardName + const alertTimeout = setTimeout(() => { + const name = sGetDashboardById(getState(), id)?.displayName + if (sGetSelectedIsLoading(getState()) && name) { dispatch( - acReceivedSnackbarMessage({ - message: loadingDashboardMsg, - open: true, - }) + acSetAlertMessage( + i18n.t('Loading dashboard – {{name}}', { name }) + ) ) } }, 500) @@ -127,9 +118,9 @@ export const tSetSelectedDashboardById = id => async (dispatch, getState) => { dispatch(acSetSelectedIsLoading(false)) - clearTimeout(snackbarTimeout) + clearTimeout(alertTimeout) - dispatch(acCloseSnackbar()) + dispatch(acClearAlertMessage()) return selected } diff --git a/src/actions/snackbar.js b/src/actions/snackbar.js deleted file mode 100644 index cc258af69..000000000 --- a/src/actions/snackbar.js +++ /dev/null @@ -1,10 +0,0 @@ -import { RECEIVED_SNACKBAR_MESSAGE, CLOSE_SNACKBAR } from '../reducers/snackbar' - -export const acReceivedSnackbarMessage = value => ({ - type: RECEIVED_SNACKBAR_MESSAGE, - value, -}) - -export const acCloseSnackbar = () => ({ - type: CLOSE_SNACKBAR, -}) diff --git a/src/components/AlertBar/AlertBar.js b/src/components/AlertBar/AlertBar.js new file mode 100644 index 000000000..30b28c763 --- /dev/null +++ b/src/components/AlertBar/AlertBar.js @@ -0,0 +1,29 @@ +import React from 'react' +import { connect } from 'react-redux' +import { AlertBar, AlertStack } from '@dhis2/ui' +import PropTypes from 'prop-types' + +import { sGetAlertMessage } from '../../reducers/alert' +import { acClearAlertMessage } from '../../actions/alert' + +export const Alert = ({ message, onClose }) => + message ? ( + + + {message} + + + ) : null + +Alert.propTypes = { + message: PropTypes.string, + onClose: PropTypes.func, +} + +const mapStateToProps = state => ({ + message: sGetAlertMessage(state), +}) + +export default connect(mapStateToProps, { + onClose: acClearAlertMessage, +})(Alert) diff --git a/src/components/AlertBar/__tests__/AlertBar.spec.js b/src/components/AlertBar/__tests__/AlertBar.spec.js new file mode 100644 index 000000000..5e45463af --- /dev/null +++ b/src/components/AlertBar/__tests__/AlertBar.spec.js @@ -0,0 +1,18 @@ +import React from 'react' +import { shallow } from 'enzyme' +import toJson from 'enzyme-to-json' +import { Alert } from '../AlertBar' + +describe('AlertBar', () => { + it('renders alert message', () => { + const AlertBar = shallow( + + ) + expect(toJson(AlertBar)).toMatchSnapshot() + }) + + it('renders nothing when no message', () => { + const AlertBar = shallow() + expect(toJson(AlertBar)).toMatchSnapshot() + }) +}) diff --git a/src/components/AlertBar/__tests__/__snapshots__/AlertBar.spec.js.snap b/src/components/AlertBar/__tests__/__snapshots__/AlertBar.spec.js.snap new file mode 100644 index 000000000..db496373f --- /dev/null +++ b/src/components/AlertBar/__tests__/__snapshots__/AlertBar.spec.js.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AlertBar renders alert message 1`] = ` + + + Luke I am your father + + +`; + +exports[`AlertBar renders nothing when no message 1`] = `""`; diff --git a/src/components/App.js b/src/components/App.js index 2859b9299..104c0bce8 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -5,6 +5,15 @@ import PropTypes from 'prop-types' import i18n from '@dhis2/d2-i18n' import { CssVariables } from '@dhis2/ui' +import Dashboard from './Dashboard/Dashboard' +import AlertBar from './AlertBar/AlertBar' + +import { acReceivedUser } from '../actions/user' +import { tFetchDashboards } from '../actions/dashboards' +import { tSetControlBarRows } from '../actions/controlBar' +import { tSetShowDescription } from '../actions/selected' +import { tSetDimensions } from '../actions/dimensions' + import { EDIT, VIEW, @@ -12,13 +21,6 @@ import { PRINT, PRINT_LAYOUT, } from './Dashboard/dashboardModes' -import { acReceivedUser } from '../actions/user' -import { tFetchDashboards } from '../actions/dashboards' -import { tSetControlBarRows } from '../actions/controlBar' -import { tSetShowDescription } from '../actions/selected' -import { tSetDimensions } from '../actions/dimensions' -import Dashboard from './Dashboard/Dashboard' -import SnackbarMessage from './SnackbarMessage/SnackbarMessage' import './App.css' @@ -85,7 +87,7 @@ export class App extends Component { /> - + ) } diff --git a/src/components/ControlBar/EditBar.js b/src/components/ControlBar/EditBar.js index 46cebbdf0..1e9f16f83 100644 --- a/src/components/ControlBar/EditBar.js +++ b/src/components/ControlBar/EditBar.js @@ -148,7 +148,6 @@ export class EditBar extends Component { onRequestClose={this.toggleTranslationDialog} objectToTranslate={this.state.dashboardModel} fieldsToTranslate={['name', 'description']} - // TODO handle messages in snackbar onTranslationSaved={this.onTranslationsSaved} onTranslationError={err => console.log('translation update error', err) diff --git a/src/components/SnackbarMessage/SnackbarMessage.js b/src/components/SnackbarMessage/SnackbarMessage.js deleted file mode 100644 index 38b280eea..000000000 --- a/src/components/SnackbarMessage/SnackbarMessage.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react' -import { connect } from 'react-redux' -import Snackbar from '@material-ui/core/Snackbar' -import PropTypes from 'prop-types' - -import { sGetSnackbar } from '../../reducers/snackbar' -import { acCloseSnackbar } from '../../actions/snackbar' - -const LOADING_DASHBOARD = 'LOADING_DASHBOARD' -export const loadingDashboardMsg = { name: '', type: LOADING_DASHBOARD } - -const SnackbarMessageContent = ({ message }) => { - if (typeof message === 'object') { - //future message types: switch(message.type) - return ( - - Loading {message.name}{' '} - dashboard - - ) - } - return message -} - -export const SnackbarMessage = props => { - return ( - } - autoHideDuration={props.snackbarDuration} - onClose={props.onCloseSnackbar} - /> - ) -} - -const mapStateToProps = state => { - const { message, duration, open } = sGetSnackbar(state) - return { - snackbarOpen: open, - snackbarMessage: message, - snackbarDuration: duration, - } -} - -SnackbarMessage.propTypes = { - snackbarDuration: PropTypes.string, - snackbarMessage: PropTypes.object, - snackbarOpen: PropTypes.bool, - onCloseSnackbar: PropTypes.func, -} - -export default connect(mapStateToProps, { - onCloseSnackbar: acCloseSnackbar, -})(SnackbarMessage) diff --git a/src/components/SnackbarMessage/__tests__/SnackbarMessage.spec.js b/src/components/SnackbarMessage/__tests__/SnackbarMessage.spec.js deleted file mode 100644 index 3d04e277c..000000000 --- a/src/components/SnackbarMessage/__tests__/SnackbarMessage.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react' -import { shallow } from 'enzyme' -import Snackbar from '@material-ui/core/Snackbar' -import { SnackbarMessage } from '../SnackbarMessage' - -describe('SnackbarMessage', () => { - let props - let shallowSnackbarMessage - const testMessage = { name: 'Luke, I am your father', type: 'Test' } - const snackbarMessage = () => { - if (!shallowSnackbarMessage) { - shallowSnackbarMessage = shallow() - } - return shallowSnackbarMessage - } - - beforeEach(() => { - props = { - snackbarOpen: false, - onCloseSnackbar: jest.fn(), - snackbarMessage: testMessage, - } - shallowSnackbarMessage = undefined - }) - - it('renders a MUI Snackbar', () => { - expect(snackbarMessage().find(Snackbar)).toHaveLength(1) - }) - - it('renders a closed MUI Snackbar', () => { - expect(snackbarMessage().prop('open')).toBeFalsy() - }) - - it('renders a MUI Snackbar with given message', () => { - expect(snackbarMessage().prop('message').props.message).toEqual( - testMessage - ) - }) -}) diff --git a/src/reducers/__tests__/alert.spec.js b/src/reducers/__tests__/alert.spec.js new file mode 100644 index 000000000..8c9b8d836 --- /dev/null +++ b/src/reducers/__tests__/alert.spec.js @@ -0,0 +1,52 @@ +import reducer, { + DEFAULT_STATE_ALERT, + SET_ALERT_MESSAGE, + CLEAR_ALERT_MESSAGE, + sGetAlertMessage, +} from '../alert' + +describe('alert reducer', () => { + it('should return the default state', () => { + const actualState = reducer(undefined, {}) + + expect(actualState).toEqual(DEFAULT_STATE_ALERT) + }) + + it('sets the alert message', () => { + const message = 'Loading dashboard: Rainbow dash' + const action = { + type: SET_ALERT_MESSAGE, + value: message, + } + + const expectedState = message + + const actualState = reducer(DEFAULT_STATE_ALERT, action) + expect(actualState).toEqual(expectedState) + }) + + it('clears the alert message', () => { + const action = { + type: CLEAR_ALERT_MESSAGE, + } + + const currentState = 'Loading dashboard: Rainbow dash' + + const actualState = reducer(currentState, action) + + expect(actualState).toEqual(DEFAULT_STATE_ALERT) + }) + + it('gets the current message from state', () => { + const message = 'Loading dashboard: Rainbow dash' + const action = { + type: SET_ALERT_MESSAGE, + value: message, + } + const alert = reducer(null, action) + + const messageInState = sGetAlertMessage({ alert }) + + expect(messageInState).toEqual(message) + }) +}) diff --git a/src/reducers/__tests__/snackbar.spec.js b/src/reducers/__tests__/snackbar.spec.js deleted file mode 100644 index 1744a5f65..000000000 --- a/src/reducers/__tests__/snackbar.spec.js +++ /dev/null @@ -1,133 +0,0 @@ -import reducer, { - DEFAULT_STATE_SNACKBAR, - RECEIVED_SNACKBAR_MESSAGE, - CLOSE_SNACKBAR, -} from '../snackbar' - -describe('snackbar reducer', () => { - it('should return the default state', () => { - const actualState = reducer(undefined, {}) - - expect(actualState).toEqual(DEFAULT_STATE_SNACKBAR) - }) - - it('should handle RECEIVED_SNACKBAR_MESSAGE action with message object containing only message text', () => { - const message = { - name: 'Loading "tinkywinky" dashboard', - type: 'LOADING_TINKYWINKY', - } - - const action = { - type: RECEIVED_SNACKBAR_MESSAGE, - value: { - message, - }, - } - - const expectedState = { - message, - duration: null, - open: false, - } - - const actualState = reducer(DEFAULT_STATE_SNACKBAR, action) - expect(actualState).toEqual(expectedState) - }) - - it('should handle RECEIVED_SNACKBAR_MESSAGE action with message object with duration previously set', () => { - const message = { - name: 'Loading "tinkywinky" dashboard', - type: 'LOADING_TINKYWINKY', - } - const duration = 3000 - - const action = { - type: RECEIVED_SNACKBAR_MESSAGE, - value: { - message, - }, - } - - const expectedState = { - message, - duration, - open: true, - } - - const currentState = { - message: 'You just won 1000 dollars', - duration, - open: true, - } - - const actualState = reducer(currentState, action) - expect(actualState).toEqual(expectedState) - }) - - it('should handle RECEIVED_SNACKBAR_MESSAGE action with message object containing text and duration', () => { - const message = { - name: 'Loading "tinkywinky" dashboard', - type: 'LOADING_TINKYWINKY', - } - const duration = 3000 - const open = true - - const action = { - type: RECEIVED_SNACKBAR_MESSAGE, - value: { - message, - duration, - open, - }, - } - - const expectedState = { - message, - duration, - open, - } - - const actualState = reducer(DEFAULT_STATE_SNACKBAR, action) - expect(actualState).toEqual(expectedState) - }) - - it('should handle RECEIVED_SNACKBAR_MESSAGE action with message string', () => { - const message = 'Loading "tinkywinky" dashboard' - const duration = 3000 - const open = true - - const action = { - type: RECEIVED_SNACKBAR_MESSAGE, - value: { - message, - duration, - open, - }, - } - - const expectedState = { - message, - duration, - open, - } - - const actualState = reducer(DEFAULT_STATE_SNACKBAR, action) - expect(actualState).toEqual(expectedState) - }) - - it('should handle the CLOSE_SNACKBAR action', () => { - const action = { - type: CLOSE_SNACKBAR, - } - - const currentState = { - message: 'You just won 1000 dollars', - duration: 3000, - open: true, - } - - const actualState = reducer(currentState, action) - - expect(actualState).toEqual(DEFAULT_STATE_SNACKBAR) - }) -}) diff --git a/src/reducers/alert.js b/src/reducers/alert.js new file mode 100644 index 000000000..c314bd887 --- /dev/null +++ b/src/reducers/alert.js @@ -0,0 +1,21 @@ +export const SET_ALERT_MESSAGE = 'SET_ALERT_MESSAGE' +export const CLEAR_ALERT_MESSAGE = 'CLEAR_ALERT_MESSAGE' + +export const DEFAULT_STATE_ALERT = null + +export default (state = DEFAULT_STATE_ALERT, action) => { + switch (action.type) { + case SET_ALERT_MESSAGE: { + return action.value + } + case CLEAR_ALERT_MESSAGE: { + return DEFAULT_STATE_ALERT + } + default: + return state + } +} + +// selectors + +export const sGetAlertMessage = state => state.alert diff --git a/src/reducers/index.js b/src/reducers/index.js index bc29ae33d..56b73270b 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -15,7 +15,7 @@ import editDashboard from './editDashboard' import printDashboard from './printDashboard' import messages from './messages' import user from './user' -import snackbar from './snackbar' +import alert from './alert' import itemFilters from './itemFilters' import style from './style' import dimensions from './dimensions' @@ -39,7 +39,7 @@ export default combineReducers({ printDashboard, itemFilters, style, - snackbar, + alert, dimensions, settings, activeModalDimension, diff --git a/src/reducers/snackbar.js b/src/reducers/snackbar.js deleted file mode 100644 index 5d80c27d4..000000000 --- a/src/reducers/snackbar.js +++ /dev/null @@ -1,25 +0,0 @@ -export const RECEIVED_SNACKBAR_MESSAGE = 'RECEIVED_SNACKBAR_MESSAGE' -export const CLOSE_SNACKBAR = 'CLOSE_SNACKBAR' - -export const DEFAULT_STATE_SNACKBAR = { - message: {}, - duration: null, - open: false, -} - -export default (state = DEFAULT_STATE_SNACKBAR, action) => { - switch (action.type) { - case RECEIVED_SNACKBAR_MESSAGE: { - return { ...state, ...action.value } - } - case CLOSE_SNACKBAR: { - return DEFAULT_STATE_SNACKBAR - } - default: - return state - } -} - -// selectors - -export const sGetSnackbar = state => state.snackbar || DEFAULT_STATE_SNACKBAR