Skip to content

Commit

Permalink
Fix geosolutions-it#1884. Add cookie policy notification
Browse files Browse the repository at this point in the history
 - Add Notification system
 - Add cookie policy epic to show the cookie policy notification
  • Loading branch information
offtherailz committed May 29, 2017
1 parent 9278b8b commit e4c076a
Show file tree
Hide file tree
Showing 16 changed files with 428 additions and 5 deletions.
71 changes: 71 additions & 0 deletions web/client/actions/__tests__/notifications-test.js
Original file line number Diff line number Diff line change
@@ -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();
});


});
51 changes: 51 additions & 0 deletions web/client/actions/notifications.js
Original file line number Diff line number Diff line change
@@ -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
};
72 changes: 72 additions & 0 deletions web/client/components/notifications/NotificationContainer.jsx
Original file line number Diff line number Diff line change
@@ -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 (<NotificationSystem ref="notify" { ...rest } />);
},
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;
Original file line number Diff line number Diff line change
@@ -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 = '<div id="container"></div>';
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(<NotificationContainer />, document.getElementById("container"));
expect(item).toExist();

});
it('creates the component with notifications', () => {
const item = ReactDOM.render(<NotificationContainer notifications={[N1]} />, 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(<NotificationContainer notifications={[N1]} />, document.getElementById("container"));
expect(item).toExist();
let elems = TestUtils.scryRenderedDOMComponentsWithClass(item, "notification");
expect(elems.length).toBe(1);

// add notification
item = ReactDOM.render(<NotificationContainer notifications={[N1, N2]} />, document.getElementById("container"));
elems = TestUtils.scryRenderedDOMComponentsWithClass(item, "notification");
expect(elems.length).toBe(2);

// remove notification
item = ReactDOM.render(<NotificationContainer notifications={[N2]} />, document.getElementById("container"));
elems = TestUtils.scryRenderedDOMComponentsWithClass(item, "notification").filter( (e) => e.className.indexOf("notification-hidden") < 0);
expect(elems.length).toBe(1);
});
});
58 changes: 58 additions & 0 deletions web/client/epics/cookies.js
Original file line number Diff line number Diff line change
@@ -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
};
4 changes: 2 additions & 2 deletions web/client/localConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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"]
}
Expand Down
21 changes: 21 additions & 0 deletions web/client/plugins/Notifications.jsx
Original file line number Diff line number Diff line change
@@ -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')
}
};
5 changes: 3 additions & 2 deletions web/client/product/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 || {},
Expand All @@ -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 || "*"),
Expand All @@ -37,6 +37,7 @@ const startApp = () => {

const appConfig = {
storeOpts,
appEpics,
appStore,
pluginsDef,
initialActions,
Expand Down
1 change: 1 addition & 0 deletions web/client/product/appConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ module.exports = {
mousePosition: {enabled: true, crs: "EPSG:4326", showCenter: true}
}
},
appEpics: require('../epics/cookies'),
storeOpts: {
persist: {
whitelist: ['security']
Expand Down
Loading

0 comments on commit e4c076a

Please sign in to comment.