Add collaboration feature

Issue-ID: SDC-767
Change-Id: I14fb4c1f54086ed03a56a7ff7fab9ecd40381795
Signed-off-by: talig <talig@amdocs.com>
diff --git a/openecomp-ui/src/sdc-app/onboarding/userNotifications/NotificationsReducer.js b/openecomp-ui/src/sdc-app/onboarding/userNotifications/NotificationsReducer.js
new file mode 100644
index 0000000..2c3442e
--- /dev/null
+++ b/openecomp-ui/src/sdc-app/onboarding/userNotifications/NotificationsReducer.js
@@ -0,0 +1,72 @@
+/*!
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import {actionTypes} from './UserNotificationsConstants.js';
+
+export default (state = {}, action) => {
+	switch (action.type) {
+		case actionTypes.NOTIFICATION:
+			let list = (state.notificationsList) ? state.notificationsList : [];
+			const {notifications, lastScanned} = action.data;
+			return {
+				...state,
+				lastScanned,
+				notificationsList: [...notifications, ...list],
+				numOfNotSeenNotifications: state.numOfNotSeenNotifications + notifications.length
+			};
+		case actionTypes.LOAD_NOTIFICATIONS:
+			return {
+				...state,
+				...action.result,
+				notificationsList: action.result.notifications,
+				notifications: undefined
+			};
+		case actionTypes.LOAD_PREV_NOTIFICATIONS:
+			const {notifications: prevNotifications, endOfPage: newEndOfPage} = action.result;
+			return {
+				...state,
+				notificationsList: [
+					...state.notificationsList,
+					...prevNotifications
+				],
+				endOfPage: newEndOfPage
+			};
+		case actionTypes.UPDATE_READ_NOTIFICATION:
+			let {notificationForUpdate} = action;
+			notificationForUpdate = {...notificationForUpdate, read: true};
+			const indexForEdit = state.notificationsList.findIndex(notification => notification.eventId === notificationForUpdate.eventId);
+			return {
+				...state,
+				notificationsList: [
+					...state.notificationsList.slice(0, indexForEdit),
+					notificationForUpdate,
+					...state.notificationsList.slice(indexForEdit + 1)
+				]
+			};
+		case actionTypes.RESET_NEW_NOTIFICATIONS:
+			return {
+				...state,
+				numOfNotSeenNotifications: 0
+			};
+		case actionTypes.TOGGLE_OVERLAY:
+			return {
+				...state,
+				showNotificationsOverlay: action.showNotificationsOverlay
+			};
+		default:
+			return state;
+	}
+};
diff --git a/openecomp-ui/src/sdc-app/onboarding/userNotifications/NotificationsView.jsx b/openecomp-ui/src/sdc-app/onboarding/userNotifications/NotificationsView.jsx
new file mode 100644
index 0000000..de105d2
--- /dev/null
+++ b/openecomp-ui/src/sdc-app/onboarding/userNotifications/NotificationsView.jsx
@@ -0,0 +1,106 @@
+/*!
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import enhanceWithClickOutside from 'react-click-outside';
+import classnames from 'classnames';
+import {connect} from 'react-redux';
+import SVGIcon from 'sdc-ui/lib/react/SVGIcon.js';
+import Overlay from 'nfvo-components/overlay/Overlay.jsx';
+import UserNotifications from 'sdc-app/onboarding/userNotifications/UserNotifications.jsx';
+import UserNotificationsActionHelper from 'sdc-app/onboarding/userNotifications/UserNotificationsActionHelper.js';
+import {actionTypes} from './UserNotificationsConstants.js';
+import OnboardingActionHelper from 'sdc-app/onboarding/OnboardingActionHelper.js';
+
+const mapStateToProps = ({currentScreen, notifications, users: {usersList}}) => {
+	return {currentScreen, notifications, usersList};
+};
+
+const mapActionToProps = (dispatch) => {
+	return {
+		resetNewNotifications: notificationId => UserNotificationsActionHelper.updateLastSeenNotification(dispatch, {notificationId}),
+		toggleOverlay: ({showNotificationsOverlay}) => dispatch({type: actionTypes.TOGGLE_OVERLAY, showNotificationsOverlay}),
+		onLoadPrevNotifications: (lastScanned, endOfPage) => UserNotificationsActionHelper.loadPreviousNotifications(dispatch, {lastScanned, endOfPage}),
+		onSync: ({itemId, itemName, versionId, versionName, currentScreen}) =>  UserNotificationsActionHelper.syncItem(dispatch, {itemId, itemName, versionId, versionName, currentScreen}),
+		updateNotification: notificationForUpdate => UserNotificationsActionHelper.updateNotification(dispatch, {notificationForUpdate}),
+		onLoadItemsLists: () => OnboardingActionHelper.loadItemsLists(dispatch)
+	};
+};
+
+
+class NotificationsView extends React.Component {
+
+	static propTypes = {
+		currentScreen: PropTypes.object,
+		notifications: PropTypes.object,
+		resetNewNotifications: PropTypes.func,
+		toggleOverlay: PropTypes.func,
+		onLoadPrevNotifications: PropTypes.func,
+		onSync: PropTypes.func,
+		updateNotification: PropTypes.func,
+		onLoadItemsLists: PropTypes.func
+	};
+
+	render() {
+		const {usersList, notifications, onLoadPrevNotifications, onSync, updateNotification, onLoadItemsLists, currentScreen} = this.props;
+		const {notificationsList, numOfNotSeenNotifications, showNotificationsOverlay, lastScanned, endOfPage} = notifications;
+
+		return (
+			<div className='onboarding-notifications'>
+				<div className='notifications-icon' onClick={() => this.onNotificationIconClick()}>
+					<SVGIcon name={numOfNotSeenNotifications > 0 ? 'notificationFullBell' : 'notificationBell'} color={numOfNotSeenNotifications > 0 ? 'primary' : ''}/>
+					<div className={classnames('notifications-count', {'hidden-count': numOfNotSeenNotifications === 0})}>
+							{numOfNotSeenNotifications}
+					</div>
+				</div>
+				{showNotificationsOverlay &&
+					<Overlay>
+						<UserNotifications notificationsList={notificationsList} usersList={usersList} lastScanned={lastScanned} endOfPage={endOfPage}
+							onLoadPrevNotifications={onLoadPrevNotifications} onSync={onSync} updateNotification={updateNotification} onLoadItemsLists={onLoadItemsLists}
+							currentScreen={currentScreen}/>
+					</Overlay>
+				}
+			</div>
+		);
+	}
+
+	handleClickOutside() {
+		const {notifications: {showNotificationsOverlay}} = this.props;
+		if(showNotificationsOverlay) {
+			this.onCloseOverlay();
+		}
+	}
+
+	onNotificationIconClick() {
+		const {notifications: {showNotificationsOverlay}, toggleOverlay} = this.props;
+		if (showNotificationsOverlay) {
+			this.onCloseOverlay();
+		} else {
+			toggleOverlay({showNotificationsOverlay: true});
+		}
+	}
+
+	onCloseOverlay() {
+		const {notifications: {numOfNotSeenNotifications, lastScanned}, resetNewNotifications, toggleOverlay} = this.props;
+		if (numOfNotSeenNotifications) {
+			resetNewNotifications(lastScanned);
+		}
+		toggleOverlay({showNotificationsOverlay: false});
+	}
+}
+
+export default connect(mapStateToProps, mapActionToProps)(enhanceWithClickOutside(NotificationsView));
diff --git a/openecomp-ui/src/sdc-app/onboarding/userNotifications/UserNotifications.jsx b/openecomp-ui/src/sdc-app/onboarding/userNotifications/UserNotifications.jsx
new file mode 100644
index 0000000..c01424e
--- /dev/null
+++ b/openecomp-ui/src/sdc-app/onboarding/userNotifications/UserNotifications.jsx
@@ -0,0 +1,131 @@
+/*!
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import ReactDOM from 'react-dom';
+import classnames from 'classnames';
+import i18n from 'nfvo-utils/i18n/i18n.js';
+import {notificationType} from './UserNotificationsConstants.js';
+import ShowMore from 'react-show-more';
+
+const Notification = ({notification, users, onActionClicked, getNotificationTypeDesc}) => {
+	const {eventType, read, eventAttributes, dateTime} = notification;
+	const {itemName, userId, description, versionName, permission, granted} = eventAttributes;
+	const {fullName: userName} = users.find(user => user.userId === userId);
+	return (
+		<div className={classnames('notification', {'unread': !read})}>
+			<div className='notification-data'>
+				<div className='item-name'>
+					{itemName}
+					{versionName && <span>&nbsp;&nbsp;&nbsp;v{versionName}</span>}
+					{!read && <div className='unread-circle-icon'></div> }
+				</div>
+				<div className='flex-items'>
+					<div className='type'>{getNotificationTypeDesc(eventType, permission, granted)}</div>
+					<div className='separator'/>
+					<div className='user-name'>{`${i18n('By')} ${userName}`}</div>
+				</div>
+				{(description || versionName) && <div className='description'>
+					{description && <ShowMore anchorClass='more-less' lines={2} more={i18n('More')} less={i18n('Less')}>
+						{description}
+					</ShowMore>}
+					{eventType === notificationType.ITEM_CHANGED.SUBMIT &&
+						<div>
+							<div>{i18n('Version {versionName} was submitted.', {versionName: versionName})}</div>
+						</div>
+					}
+				</div>
+				}
+				<div className='date'>{dateTime}</div>
+			</div>
+			<div className='notification-action'>
+				<div className={classnames('action-button', {'hidden': read})} onClick={() => onActionClicked(notification)}>
+					{eventType === notificationType.PERMISSION_CHANGED ? i18n('Accept') : i18n('Sync')}
+				</div>
+			</div>
+		</div>
+	);
+};
+
+function getNotificationTypeDesc(eventType, permission, granted) {
+	switch (eventType) {
+		case notificationType.PERMISSION_CHANGED:
+			return i18n('Permission {granted}: {permission}', {granted: granted ? 'Granted' : 'Taken', permission: permission});
+		case notificationType.ITEM_CHANGED.COMMIT:
+			return i18n('Your Copy Is Out Of Sync');
+		case notificationType.ITEM_CHANGED.SUBMIT:
+			return i18n('Version Submitted');
+	}
+}
+
+class UserNotifications extends React.Component {
+
+	static propTypes = {
+		currentScreen: PropTypes.object,
+		notificationsList: PropTypes.array,
+		usersList: PropTypes.array,
+		lastScanned: PropTypes.string,
+		endOfPage:PropTypes.string,
+		onLoadPrevNotifications: PropTypes.func,
+		onSync: PropTypes.func,
+		updateNotification: PropTypes.func,
+		onLoadItemsLists: PropTypes.func
+	};
+
+	render() {
+		const {notificationsList = [], usersList, lastScanned, endOfPage} = this.props;
+
+		return (
+			<div className='user-notifications'>
+				<div className='notifications-title'>{i18n('Notifications')}</div>
+				<div className='notifications-list' ref='notificationList' onScroll={() => this.loadPrevNotifications(lastScanned, endOfPage)}>
+				{
+					notificationsList.map(notification => (
+						<Notification key={notification.eventId} notification={notification} users={usersList}
+							onActionClicked={notification => this.onActionClicked(notification)}
+							getNotificationTypeDesc={getNotificationTypeDesc}/>))
+				}
+				</div>
+			</div>
+		);
+	}
+
+	onActionClicked(notification) {
+		const {onSync, updateNotification, currentScreen, onLoadItemsLists} = this.props;
+		const {eventType, eventAttributes: {itemId, itemName, versionId, versionName}} = notification;
+		if(eventType !== notificationType.PERMISSION_CHANGED) {
+			onSync({itemId, itemName, versionId, versionName, currentScreen});
+		}
+		else {
+			onLoadItemsLists();
+		}
+		updateNotification(notification);
+	}
+
+	loadPrevNotifications(lastScanned, endOfPage) {
+		if(endOfPage && lastScanned) {
+			let element = ReactDOM.findDOMNode(this.refs['notificationList']);
+			const {onLoadPrevNotifications} = this.props;
+
+			if (element && element.clientHeight + element.scrollTop === element.scrollHeight) {
+				onLoadPrevNotifications(lastScanned, endOfPage);
+			}
+		}
+	}
+}
+
+export default UserNotifications;
diff --git a/openecomp-ui/src/sdc-app/onboarding/userNotifications/UserNotificationsActionHelper.js b/openecomp-ui/src/sdc-app/onboarding/userNotifications/UserNotificationsActionHelper.js
new file mode 100644
index 0000000..574aa0f
--- /dev/null
+++ b/openecomp-ui/src/sdc-app/onboarding/userNotifications/UserNotificationsActionHelper.js
@@ -0,0 +1,123 @@
+import {actionTypes} from './UserNotificationsConstants.js';
+import i18n from 'nfvo-utils/i18n/i18n.js';
+import Configuration from 'sdc-app/config/Configuration.js';
+import RestAPIUtil from 'nfvo-utils/RestAPIUtil.js';
+import WebSocketUtil, {websocketUrl} from 'nfvo-utils/WebSocketUtil.js';
+import {actionsEnum as VersionControllerActionsEnum} from 'nfvo-components/panel/versionController/VersionControllerConstants.js';
+import ItemsHelper from 'sdc-app/common/helpers/ItemsHelper.js';
+import ScreensHelper from 'sdc-app/common/helpers/ScreensHelper.js';
+import MergeEditorActionHelper from 'sdc-app/common/merge/MergeEditorActionHelper.js';
+import {actionTypes as modalActionTypes} from 'nfvo-components/modal/GlobalModalConstants.js';
+import {SyncStates} from 'sdc-app/common/merge/MergeEditorConstants.js';
+
+function baseUrl() {
+	const restPrefix = Configuration.get('restPrefix');
+	return `${restPrefix}/v1.0/notifications`;
+}
+
+function fetch() {
+	return RestAPIUtil.fetch(baseUrl());
+}
+
+function updateNotification(notificationId) {
+	return RestAPIUtil.put(`${baseUrl()}/${notificationId}`);
+}
+
+function updateLastSeenNotification(notificationId) {
+	return RestAPIUtil.put(`${baseUrl()}/last-seen/${notificationId}`);
+}
+
+function loadPrevNotifications(lastScanned, endOfPage) {
+	return RestAPIUtil.fetch(`${baseUrl()}?LAST_DELIVERED_EVENT_ID=${lastScanned}&END_OF_PAGE_EVENT_ID=${endOfPage}`);
+}
+
+const INITIAL_LAST_SCANNED = '00000000-0000-1000-8080-808080808080';
+
+const UserNotificationsActionHelper = {
+	notificationsFirstHandling(dispatch) {
+		console.log('Websocket Url: ', websocketUrl);
+		UserNotificationsActionHelper.fetchUserNotificationsList(dispatch).then(({lastScanned}) => {
+			WebSocketUtil.open(websocketUrl, {lastScanned: lastScanned || INITIAL_LAST_SCANNED});
+		});
+	},
+
+	fetchUserNotificationsList(dispatch) {
+		return fetch().then(result => {
+			dispatch({
+				type: actionTypes.LOAD_NOTIFICATIONS,
+				result
+			});
+			return Promise.resolve({lastScanned: result.lastScanned});
+		});
+	},
+
+	loadPreviousNotifications(dispatch, {lastScanned, endOfPage}) {
+		loadPrevNotifications(lastScanned, endOfPage).then(result => dispatch({
+			type: actionTypes.LOAD_PREV_NOTIFICATIONS,
+			result
+		}));
+	},
+
+	notifyAboutConflicts(dispatch, {itemId, itemName, version, currentScreen}) {
+		let {props} = currentScreen;
+		let currentItemId = props.softwareProductId || props.licenseModelId;
+		let currentVersion = props.version;
+		if(currentItemId === itemId && currentVersion.id === version.id) {
+			MergeEditorActionHelper.analyzeSyncResult(dispatch, {itemId, version}).then(() => ScreensHelper.loadScreen(dispatch, currentScreen));
+		}
+		else {
+			dispatch({
+				type: modalActionTypes.GLOBAL_MODAL_WARNING,
+				data: {
+					title: i18n('Conflicts'),
+					msg: i18n('There are conflicts in {itemName} version {versionName} that you have to resolve', {itemName: itemName.toUpperCase(), versionName: version.versionName}),
+					cancelButtonText: i18n('OK')
+				}
+			});
+		}
+	},
+
+	syncItem(dispatch, {itemId, itemName, versionId, versionName, currentScreen}) {
+		let version = {id: versionId, versionName};
+		ItemsHelper.fetchVersion({itemId, versionId}).then(response => {
+			let inMerge = response && response.state && response.state.synchronizationState === SyncStates.MERGE;
+			if (!inMerge) {
+				ItemsHelper.performVCAction({itemId, version, action: VersionControllerActionsEnum.SYNC}).then(() => {
+					return ItemsHelper.fetchVersion({itemId, versionId}).then(response => {
+						let inMerge = response && response.state && response.state.synchronizationState === SyncStates.MERGE;
+						if (!inMerge) {
+							return ScreensHelper.loadScreen(dispatch, currentScreen);
+						}
+						else {
+							return this.notifyAboutConflicts(dispatch, {itemId, itemName, version, currentScreen});
+						}
+					});
+				});
+			}
+			else {
+				this.notifyAboutConflicts(dispatch, {itemId, itemName, version, currentScreen});
+			}
+		});
+	},
+
+	updateNotification(dispatch, {notificationForUpdate}) {
+		updateNotification(notificationForUpdate.eventId).then(response => {
+			if(response.status === 'Success' && Object.keys(response.errors).length === 0) {
+				dispatch({
+					type: actionTypes.UPDATE_READ_NOTIFICATION,
+					notificationForUpdate
+				});
+			}
+		});
+	},
+
+	updateLastSeenNotification(dispatch, {notificationId}) {
+		updateLastSeenNotification(notificationId).then(response => {
+			if (response.status === 'Success' && Object.keys(response.errors).length === 0) {
+				dispatch({type: actionTypes.RESET_NEW_NOTIFICATIONS});
+			}
+		});
+	}
+};
+
+export default UserNotificationsActionHelper;
diff --git a/openecomp-ui/src/sdc-app/onboarding/userNotifications/UserNotificationsConstants.js b/openecomp-ui/src/sdc-app/onboarding/userNotifications/UserNotificationsConstants.js
new file mode 100644
index 0000000..f006b5a
--- /dev/null
+++ b/openecomp-ui/src/sdc-app/onboarding/userNotifications/UserNotificationsConstants.js
@@ -0,0 +1,19 @@
+
+import keyMirror from 'nfvo-utils/KeyMirror.js';
+
+export const actionTypes = keyMirror({
+	NOTIFICATION: null,
+	LOAD_NOTIFICATIONS: null,
+	LOAD_PREV_NOTIFICATIONS: null,
+	UPDATE_READ_NOTIFICATION: null,
+	RESET_NEW_NOTIFICATIONS: null,
+	TOGGLE_OVERLAY: null
+});
+
+export const notificationType = keyMirror({
+	PERMISSION_CHANGED: 'PermissionChanged',
+	ITEM_CHANGED: {
+		COMMIT: 'commit',
+		SUBMIT: 'submit'
+	}
+});
\ No newline at end of file