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> 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