From e4c076ae02a30eab3edaf3902f73d0398d1c8b84 Mon Sep 17 00:00:00 2001 From: Lorenzo Natali Date: Mon, 29 May 2017 16:04:04 +0200 Subject: [PATCH] Fix #1884. Add cookie policy notification - Add Notification system - Add cookie policy epic to show the cookie policy notification --- .../actions/__tests__/notifications-test.js | 71 ++++++++++++++++++ web/client/actions/notifications.js | 51 +++++++++++++ .../notifications/NotificationContainer.jsx | 72 +++++++++++++++++++ .../__tests__/NotificationContainer-test.jsx | 69 ++++++++++++++++++ web/client/epics/cookies.js | 58 +++++++++++++++ web/client/localConfig.json | 4 +- web/client/plugins/Notifications.jsx | 21 ++++++ web/client/product/app.jsx | 5 +- web/client/product/appConfig.js | 1 + web/client/product/plugins.js | 3 +- .../reducers/__tests__/notifications-test.js | 27 +++++++ web/client/reducers/notifications.js | 30 ++++++++ web/client/translations/data.de-DE | 5 ++ web/client/translations/data.en-US | 5 ++ web/client/translations/data.fr-FR | 6 ++ web/client/translations/data.it-IT | 5 ++ 16 files changed, 428 insertions(+), 5 deletions(-) create mode 100644 web/client/actions/__tests__/notifications-test.js create mode 100644 web/client/actions/notifications.js create mode 100644 web/client/components/notifications/NotificationContainer.jsx create mode 100644 web/client/components/notifications/__tests__/NotificationContainer-test.jsx create mode 100644 web/client/epics/cookies.js create mode 100644 web/client/plugins/Notifications.jsx create mode 100644 web/client/reducers/__tests__/notifications-test.js create mode 100644 web/client/reducers/notifications.js diff --git a/web/client/actions/__tests__/notifications-test.js b/web/client/actions/__tests__/notifications-test.js new file mode 100644 index 0000000000..fca51cd439 --- /dev/null +++ b/web/client/actions/__tests__/notifications-test.js @@ -0,0 +1,71 @@ +/* + * Copyright 2017, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +var expect = require('expect'); +var { + SHOW_NOTIFICATION, + HIDE_NOTIFICATION, + CLEAR_NOTIFICATIONS, + show, + success, + warning, + error, + info, + hide, + clear +} = require('../notifications'); + +describe('Test correctness of the notifications actions', () => { + + it('show', () => { + const action = show({title: "test"}); + expect(action.type).toBe(SHOW_NOTIFICATION); + expect(action.title).toBe('test'); + expect(action.level).toBe('success'); + expect(action.uid).toExist(); + }); + it('hide', () => { + const action = hide("1234"); + expect(action.type).toBe(HIDE_NOTIFICATION); + expect(action.uid).toBe('1234'); + }); + it('clear', () => { + const action = clear(); + expect(action.type).toBe(CLEAR_NOTIFICATIONS); + }); + it('success', () => { + const action = success({title: "test"}); + expect(action.type).toBe(SHOW_NOTIFICATION); + expect(action.title).toBe('test'); + expect(action.level).toBe('success'); + expect(action.uid).toExist(); + }); + it('warning', () => { + const action = warning({title: "test"}); + expect(action.type).toBe(SHOW_NOTIFICATION); + expect(action.title).toBe('test'); + expect(action.level).toBe('warning'); + expect(action.uid).toExist(); + }); + it('error', () => { + const action = error({title: "test"}); + expect(action.type).toBe(SHOW_NOTIFICATION); + expect(action.title).toBe('test'); + expect(action.level).toBe('error'); + expect(action.uid).toExist(); + }); + it('info', () => { + const action = info({title: "test"}); + expect(action.type).toBe(SHOW_NOTIFICATION); + expect(action.title).toBe('test'); + expect(action.level).toBe('info'); + expect(action.uid).toExist(); + }); + + +}); diff --git a/web/client/actions/notifications.js b/web/client/actions/notifications.js new file mode 100644 index 0000000000..88301b9145 --- /dev/null +++ b/web/client/actions/notifications.js @@ -0,0 +1,51 @@ +const SHOW_NOTIFICATION = 'SHOW_NOTIFICATION'; +const HIDE_NOTIFICATION = 'HIDE_NOTIFICATION'; +const CLEAR_NOTIFICATIONS = 'CLEAR_NOTIFICATIONS'; + +function show(opts = {}, level = 'success') { + return { + type: SHOW_NOTIFICATION, + ...opts, + uid: opts.uid || Date.now(), + level + }; +} +function hide(uid) { + return { + type: HIDE_NOTIFICATION, + uid + }; +} +function success(opts) { + return show(opts, 'success'); +} + +function error(opts) { + return show(opts, 'error'); +} + +function warning(opts) { + return show(opts, 'warning'); +} + +function info(opts) { + return show(opts, 'info'); +} + +function clear() { + return { + type: CLEAR_NOTIFICATIONS + }; +} +module.exports = { + SHOW_NOTIFICATION, + HIDE_NOTIFICATION, + CLEAR_NOTIFICATIONS, + show, + success, + warning, + error, + info, + hide, + clear +}; diff --git a/web/client/components/notifications/NotificationContainer.jsx b/web/client/components/notifications/NotificationContainer.jsx new file mode 100644 index 0000000000..0643606eb1 --- /dev/null +++ b/web/client/components/notifications/NotificationContainer.jsx @@ -0,0 +1,72 @@ +/* + * Copyright 2017, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require('react'); +const NotificationSystem = require('react-notification-system'); +var LocaleUtils = require('../../utils/LocaleUtils'); + +const NotificationContainer = React.createClass({ + propTypes: { + notifications: React.PropTypes.array, + onRemove: React.PropTypes.func + }, + contextTypes: { + messages: React.PropTypes.object + }, + getDefaultProps() { + return { + notifications: [], + onRemove: () => {} + }; + }, + componentDidMount() { + this.updateNotifications(this.props.notifications); + }, + shouldComponentUpdate(nextProps) { + return this.props !== nextProps; + }, + componentDidUpdate() { + const {notifications} = this.props || []; + this.updateNotifications(notifications); + }, + render() { + const {notifications, onRemove, ...rest} = this.props; + return (); + }, + system() { + return this.refs.notify; + }, + updateNotifications(notifications) { + const notificationIds = notifications.map(notification => notification.uid); + const systemNotifications = this.system().state.notifications || []; + // Get all active notifications from react-notification-system + // and remove all where uid is not found in the reducer + systemNotifications.forEach(notification => { + if (notificationIds.indexOf(notification.uid) < 0) { + this.system().removeNotification(notification.uid); + } + }); + notifications.forEach(notification => { + if (systemNotifications.indexOf(notification.uid) < 0) { + this.system().addNotification({ + ...notification, + title: LocaleUtils.getMessageById(this.context.messages, notification.title) || notification.title, + message: LocaleUtils.getMessageById(this.context.messages, notification.message) || notification.message, + action: notification.action && { + label: LocaleUtils.getMessageById(this.context.messages, notification.action.label) || notification.action.label + }, + onRemove: () => { + this.props.onRemove(notification.uid); + if (notification.onRemove) notification.onRemove(); + } + }); + } + }); + } +}); + +module.exports = NotificationContainer; diff --git a/web/client/components/notifications/__tests__/NotificationContainer-test.jsx b/web/client/components/notifications/__tests__/NotificationContainer-test.jsx new file mode 100644 index 0000000000..2da527c9e0 --- /dev/null +++ b/web/client/components/notifications/__tests__/NotificationContainer-test.jsx @@ -0,0 +1,69 @@ +/* + * Copyright 2017, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require('react'); +const ReactDOM = require('react-dom'); +const NotificationContainer = require('../NotificationContainer.jsx'); +const expect = require('expect'); + +const TestUtils = require('react-addons-test-utils'); +const N1 = { + uid: "1", + title: "test 1", + message: "test 1", + autodismiss: 0, + level: "success" +}; + +const N2 = { + uid: "2", + title: "test 2", + message: "test 2", + autodismiss: 0, + level: "success" +}; + +describe('NotificationContainer tests', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + // test DEFAULTS + it('creates the component with defaults', () => { + const item = ReactDOM.render(, document.getElementById("container")); + expect(item).toExist(); + + }); + it('creates the component with notifications', () => { + const item = ReactDOM.render(, document.getElementById("container")); + expect(item).toExist(); + let elems = TestUtils.scryRenderedDOMComponentsWithClass(item, "notifications-tr"); + expect(elems.length).toBe(1); + }); + it('update notifications', () => { + let item = ReactDOM.render(, document.getElementById("container")); + expect(item).toExist(); + let elems = TestUtils.scryRenderedDOMComponentsWithClass(item, "notification"); + expect(elems.length).toBe(1); + + // add notification + item = ReactDOM.render(, document.getElementById("container")); + elems = TestUtils.scryRenderedDOMComponentsWithClass(item, "notification"); + expect(elems.length).toBe(2); + + // remove notification + item = ReactDOM.render(, document.getElementById("container")); + elems = TestUtils.scryRenderedDOMComponentsWithClass(item, "notification").filter( (e) => e.className.indexOf("notification-hidden") < 0); + expect(elems.length).toBe(1); + }); +}); diff --git a/web/client/epics/cookies.js b/web/client/epics/cookies.js new file mode 100644 index 0000000000..6e57028788 --- /dev/null +++ b/web/client/epics/cookies.js @@ -0,0 +1,58 @@ +/* + * Copyright 2017, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const {info, HIDE_NOTIFICATION} = require('../actions/notifications'); +const Rx = require('rxjs'); +const {head} = require('lodash'); +import { UPDATE_LOCATION } from 'react-router-redux'; + + +const COOKIE_NOTIFICATION_ID = "cookiesPolicyNotification"; +const cookieNotificationSelector = (state) => state && state.notifications && head(state.notifications.filter( n => n.uid === COOKIE_NOTIFICATION_ID)); + +/** + * Show the cookie policy notification + * @param {external:Observable} action$ triggers on "UPDATE_LOCATION" + * @param {object} store the store, to get current notifications + * @memberof epics.cookies + * @return {external:Observable} the steam of actions to trigger to display the noitification. + */ +const cookiePolicyChecker = (action$, store) => + action$.ofType(UPDATE_LOCATION) + .take(1) + .filter( () => !localStorage.getItem("cookies-policy-approved") && !cookieNotificationSelector(store.getState())) + .switchMap(() => + Rx.Observable.of(info({ + uid: COOKIE_NOTIFICATION_ID, + title: "cookiesPolicyNotification.title", + message: "cookiesPolicyNotification.message", + action: { + label: "cookiesPolicyNotification.confirm" + }, + autoDismiss: 0, + position: "bl" + })) + ); + +const cookiePolicyDismiss = (action$) => + action$.ofType(HIDE_NOTIFICATION) + .switchMap( (action) => { + if (action.uid === COOKIE_NOTIFICATION_ID ) { + localStorage.setItem("cookies-policy-approved", true); + } + return Rx.Observable.empty(); + }); + +/** + * Epics for cookies policy informations + * @name epics.cookies + * @type {Object} + */ +module.exports = { + cookiePolicyChecker, + cookiePolicyDismiss +}; diff --git a/web/client/localConfig.json b/web/client/localConfig.json index 9d0cf6b848..3b5bb27803 100644 --- a/web/client/localConfig.json +++ b/web/client/localConfig.json @@ -127,7 +127,7 @@ }, "Login", "OmniBar", "BurgerMenu", "Expander", "GlobeViewSwitcher" ], - "desktop": ["Map", "HelpLink", "Share", "DrawerMenu", "Version", {"name": "BackgroundSelector", "cfg": { "bottom": 40 } }, + "desktop": ["Map", "HelpLink", "Share", "DrawerMenu", "Version", "Notifications", {"name": "BackgroundSelector", "cfg": { "bottom": 40 } }, { "name": "Identify", "showIn": ["IdentifyBar", "Settings"], @@ -307,7 +307,7 @@ "cfg": { "className": "navbar shadow navbar-home" } - }, "ManagerMenu", "Login", "Language", "Attribution", "ScrollTop"], + }, "ManagerMenu", "Login", "Language", "Attribution", "ScrollTop", "Notifications"], "maps": ["Header", "Fork", "MapSearch", "HomeDescription", "MapType", "ThemeSwitcher", "GridContainer", "CreateNewMap", "Maps", "Examples", "Footer"], "manager": ["Header", "Redirect", "Manager", "Home", "UserManager", "GroupManager", "Footer"] } diff --git a/web/client/plugins/Notifications.jsx b/web/client/plugins/Notifications.jsx new file mode 100644 index 0000000000..6e187ceee2 --- /dev/null +++ b/web/client/plugins/Notifications.jsx @@ -0,0 +1,21 @@ +/** + * Copyright 2016, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +const {hide} = require('../actions/notifications'); +const {connect} = require('react-redux'); + + +module.exports = { + NotificationsPlugin: connect( + (state) => ({ notifications: state && state.notifications}), + {onRemove: hide} + )(require('../components/notifications/NotificationContainer')), + reducers: { + notifications: require('../reducers/notifications') + } +}; diff --git a/web/client/product/app.jsx b/web/client/product/app.jsx index d445b93d7d..2270548ab0 100644 --- a/web/client/product/app.jsx +++ b/web/client/product/app.jsx @@ -18,7 +18,7 @@ const startApp = () => { const StandardApp = require('../components/app/StandardApp'); - const {pages, pluginsDef, initialState, storeOpts} = require('./appConfig'); + const {pages, pluginsDef, initialState, storeOpts, appEpics = {}} = require('./appConfig'); const StandardRouter = connect((state) => ({ locale: state.locale || {}, @@ -28,7 +28,7 @@ const startApp = () => { const appStore = require('../stores/StandardStore').bind(null, initialState, { maptype: require('../reducers/maptype'), maps: require('../reducers/maps') - }, {}); + }, appEpics); const initialActions = [ () => loadMaps(ConfigUtils.getDefaults().geoStoreUrl, ConfigUtils.getDefaults().initialMapFilter || "*"), @@ -37,6 +37,7 @@ const startApp = () => { const appConfig = { storeOpts, + appEpics, appStore, pluginsDef, initialActions, diff --git a/web/client/product/appConfig.js b/web/client/product/appConfig.js index 9bcc57547f..87c5e5231f 100644 --- a/web/client/product/appConfig.js +++ b/web/client/product/appConfig.js @@ -54,6 +54,7 @@ module.exports = { mousePosition: {enabled: true, crs: "EPSG:4326", showCenter: true} } }, + appEpics: require('../epics/cookies'), storeOpts: { persist: { whitelist: ['security'] diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js index 48cd043c62..3c368ad7ed 100644 --- a/web/client/product/plugins.js +++ b/web/client/product/plugins.js @@ -70,7 +70,8 @@ module.exports = { GlobeViewSwitcherPlugin: require('../plugins/GlobeViewSwitcher'), BackgroundSelectorPlugin: require('../plugins/BackgroundSelector'), SearchServicesConfigPlugin: require('../plugins/SearchServicesConfig'), - VersionPlugin: require('../plugins/Version') + VersionPlugin: require('../plugins/Version'), + NotificationsPlugin: require('../plugins/Notifications') }, requires: { ReactSwipe: require('react-swipeable-views').default, diff --git a/web/client/reducers/__tests__/notifications-test.js b/web/client/reducers/__tests__/notifications-test.js new file mode 100644 index 0000000000..6e7d1e22af --- /dev/null +++ b/web/client/reducers/__tests__/notifications-test.js @@ -0,0 +1,27 @@ +/* + * Copyright 2017, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +var expect = require('expect'); +const {show, hide, clear} = require('../../actions/notifications'); +const notifications = require('../notifications'); +describe('Test the notifications reducer', () => { + it('add notification', () => { + let state = notifications([], show({title: "test", uid: 1})); + expect(state).toExist(); + expect(state.length).toBe(1); + }); + it('hide notification', () => { + let state = notifications([{uid: 1}], hide(1)); + expect(state).toExist(); + expect(state.length).toBe(0); + }); + it('clear notifications', () => { + let state = notifications([{uid: 1}], clear()); + expect(state).toExist(); + expect(state.length).toBe(0); + }); +}); diff --git a/web/client/reducers/notifications.js b/web/client/reducers/notifications.js new file mode 100644 index 0000000000..337d4502d2 --- /dev/null +++ b/web/client/reducers/notifications.js @@ -0,0 +1,30 @@ +/* + * Copyright 2017, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +const {SHOW_NOTIFICATION, HIDE_NOTIFICATION, CLEAR_NOTIFICATIONS} = require('../actions/notifications'); + +function notifications(state = [], action = {}) { + switch (action.type) { + case SHOW_NOTIFICATION: + const { type, ...rest } = action; + return [ + ...state, + { ...rest, uid: action.uid} + ]; + case HIDE_NOTIFICATION: + return state.filter(notification => { + return notification.uid !== action.uid; + }); + case CLEAR_NOTIFICATIONS: + return []; + default: + return state; + } + return state; +} +module.exports = notifications; diff --git a/web/client/translations/data.de-DE b/web/client/translations/data.de-DE index 41e9e67495..a60937c1e3 100644 --- a/web/client/translations/data.de-DE +++ b/web/client/translations/data.de-DE @@ -130,6 +130,11 @@ } } }, + "cookiesPolicyNotification": { + "title": "Deze website maakt gebruik van cookies", + "message": "Door onze website te blijven gebruiken, accepteert u ons gebruik van cookies.", + "confirm": "Ik ben het eens" + }, "manager": { "openInANewTab":"Karte öffnen", "deleteMap":"Karte löschen", diff --git a/web/client/translations/data.en-US b/web/client/translations/data.en-US index 683f841def..8d34f6a12c 100644 --- a/web/client/translations/data.en-US +++ b/web/client/translations/data.en-US @@ -130,6 +130,11 @@ } } }, + "cookiesPolicyNotification": { + "title": "This website uses cookies", + "message": "By continuing to use our website you are accepting our use of cookies.", + "confirm": "I Agree" + }, "manager": { "openInANewTab":"Open map", "deleteMap":"Delete Map", diff --git a/web/client/translations/data.fr-FR b/web/client/translations/data.fr-FR index 2dc03d882e..f01f4545ca 100644 --- a/web/client/translations/data.fr-FR +++ b/web/client/translations/data.fr-FR @@ -1,3 +1,4 @@ + { "locale": "fr-FR", "messages": { @@ -131,6 +132,11 @@ } } }, + "cookiesPolicyNotification": { + "title": "Ce site utilise des cookies", + "message": "En continuant d'utiliser notre site Web, vous acceptez notre utilisation de cookies.", + "confirm": "Je suis d'accord" + }, "manager": { "openInANewTab":"Ouvrir la carte", "deleteMap":"Supprimer la carte", diff --git a/web/client/translations/data.it-IT b/web/client/translations/data.it-IT index 98e8f65c62..34de0e2815 100644 --- a/web/client/translations/data.it-IT +++ b/web/client/translations/data.it-IT @@ -130,6 +130,11 @@ } } }, + "cookiesPolicyNotification": { + "title": "Questo sito utilizza cookies", + "message": "Continuando a navigare sul sito accetti all'uso dei cookie", + "confirm": "Acconsento" + }, "manager": { "openInANewTab":"Apri mappa", "deleteMap":"Elimina Mappa",