diff --git a/client/src/actions/__tests__/session-actions-test.js b/client/src/actions/__tests__/session-actions-test.js index 642f1fa4..432970a9 100644 --- a/client/src/actions/__tests__/session-actions-test.js +++ b/client/src/actions/__tests__/session-actions-test.js @@ -11,42 +11,55 @@ const SessionActions = requireUnit('actions/session-actions', { }); describe('Session Actions,', function () { - APICallMock.call.returns('API_RESULT'); describe('login action', function () { - it('should return LOGIN with with API_RESULT promise', function () { - APICallMock.call.reset(); + it('should return LOGIN with with a result promise', function () { + APICallMock.call.returns({ + then: function (resolve) { + resolve({ + data: { + userId: 14 + } + }); + } + }); + let loginData = { email: 'SOME_EMAIL', password: 'SOME_PASSWORD', remember: false }; - expect(SessionActions.login(loginData)).to.deep.equal({ - type: 'LOGIN', - payload: 'API_RESULT' - }); - + expect(SessionActions.login(loginData).type).to.equal('LOGIN'); + expect(storeMock.dispatch).to.have.been.calledWithMatch({type: 'USER_DATA'}); expect(APICallMock.call).to.have.been.calledWith({ - path: '/user/login', - data: loginData + path: '/user/get', + data: { + userId: 14 + } }); }); }); describe('autoLogin action', function () { it('should return LOGIN_AUTO with remember data from sessionStore', function () { - APICallMock.call.reset(); + APICallMock.call.returns({ + then: function (resolve) { + resolve({ + data: { + userId: 14 + } + }); + } + }); sessionStoreMock.getRememberData.returns({ token: 'SOME_TOKEN', userId: 'SOME_ID', expiration: 'SOME_EXPIRATION' }); - expect(SessionActions.autoLogin()).to.deep.equal({ - type: 'LOGIN_AUTO', - payload: 'API_RESULT' - }); + expect(SessionActions.autoLogin().type).to.equal('LOGIN_AUTO'); + expect(storeMock.dispatch).to.have.been.calledWithMatch({type: 'USER_DATA'}); expect(APICallMock.call).to.have.been.calledWith({ path: '/user/login', data: { @@ -60,6 +73,7 @@ describe('Session Actions,', function () { describe('logout action', function () { it('should return LOGOUT and call /user/logout', function () { + APICallMock.call.returns('API_RESULT'); APICallMock.call.reset(); expect(SessionActions.logout()).to.deep.equal({ diff --git a/client/src/actions/session-actions.js b/client/src/actions/session-actions.js index f4ca14fc..ed9490b4 100644 --- a/client/src/actions/session-actions.js +++ b/client/src/actions/session-actions.js @@ -9,6 +9,10 @@ export default { payload: API.call({ path: '/user/login', data: loginData + }).then((result) => { + store.dispatch(this.getUserData(result.data.userId)); + + return result; }) }; }, @@ -25,6 +29,10 @@ export default { rememberToken: rememberData.token, isAutomatic: true } + }).then((result) => { + store.dispatch(this.getUserData(result.data.userId)); + + return result; }) }; }, @@ -39,6 +47,18 @@ export default { }; }, + getUserData(userId) { + return { + type: 'USER_DATA', + payload: API.call({ + path: '/user/get', + data: { + userId: userId + } + }) + } + }, + initSession() { return { type: 'CHECK_SESSION', diff --git a/client/src/app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page.js b/client/src/app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page.js index c38befbb..184bf375 100644 --- a/client/src/app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page.js +++ b/client/src/app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page.js @@ -1,90 +1,16 @@ import React from 'react'; +import {connect} from 'react-redux'; + import Table from 'core-components/table'; import Button from 'core-components/button'; -let mockTickets = [ - { - ticketNumber: '445441', - title: 'Problem with installation', - content: 'I had a problem with the installation of the php server', - department: 'Environment Setup', - date: '15 Apr 2016', - file: 'http://www.opensupports.com/some_file.zip', - language: 'en', - unread: true, - closed: false, - author: { - name: 'John Smith', - email: 'john@smith.com' - }, - owner: { - name: 'Steve Jobs' - }, - comments: [ - { - content: 'Do you have apache installed? It generally happens if you dont have apache.', - author: { - name: 'Steve Jobs', - email: 'jobs@steve.com', - staff: true - } - }, - { - content: 'I have already installed apache, but the problem persists', - author: { - name: 'John Smith', - steve: 'john@smith.com', - staff: false - } - } - ] - }, - { - ticketNumber: '87852', - title: 'Lorem ipsum door', - content: 'I had a problem with the installation of the php server', - department: 'Environment Setup', - date: '15 Apr 2016', - file: 'http://www.opensupports.com/some_file.zip', - language: 'en', - unread: false, - closed: false, - author: { - name: 'John Smith', - email: 'john@smith.com' - }, - owner: { - name: 'Steve Jobs' - }, - comments: [ - { - content: 'Do you have apache installed? It generally happens if you dont have apache.', - author: { - name: 'Steve Jobs', - email: 'jobs@steve.com', - staff: true - } - }, - { - content: 'I have already installed apache, but the problem persists', - author: { - name: 'John Smith', - steve: 'john@smith.com', - staff: false - } - } - ] - } -]; - - class DashboardListTicketsPage extends React.Component { static propTypes = { tickets: React.PropTypes.arrayOf(React.PropTypes.object) }; static defaultProps = { - tickets: mockTickets.concat([mockTickets[1], mockTickets[1]]) + tickets: [] }; render() { @@ -135,11 +61,16 @@ class DashboardListTicketsPage extends React.Component { {titleText} ), - department: ticket.department, + department: ticket.department.name, date: ticket.date, highlighted: ticket.unread }; } } -export default DashboardListTicketsPage; + +export default connect((store) => { + return { + tickets: store.session.userTickets + }; +})(DashboardListTicketsPage); diff --git a/client/src/app/main/dashboard/dashboard-menu.js b/client/src/app/main/dashboard/dashboard-menu.js index e0a656b1..a44c470f 100644 --- a/client/src/app/main/dashboard/dashboard-menu.js +++ b/client/src/app/main/dashboard/dashboard-menu.js @@ -1,14 +1,11 @@ import React from 'react'; import _ from 'lodash'; -import Menu from 'core-components/menu'; +import {dispatch} from 'app/store'; +import SessionActions from 'actions/session-actions'; +import i18n from 'lib-app/i18n'; -let dashboardRoutes = [ - { path: '/dashboard', text: 'Ticket List', icon: 'file-text-o' }, - { path: '/dashboard/create-ticket', text: 'Create Ticket', icon: 'plus' }, - { path: '/dashboard/articles', text: 'View Articles', icon: 'book' }, - { path: '/dashboard/edit-profile', text: 'Edit Profile', icon: 'pencil' } -]; +import Menu from 'core-components/menu'; class DashboardMenu extends React.Component { static contextTypes = { @@ -30,14 +27,18 @@ class DashboardMenu extends React.Component { header: 'Dashboard', items: this.getMenuItems(), selectedIndex: this.getSelectedIndex(), - onItemClick: this.goToPathByIndex.bind(this), + onItemClick: this.onItemClick.bind(this), tabbable: true, type: 'secondary' }; } getMenuItems() { - return dashboardRoutes.map(this.getMenuItem.bind(this)); + let items = this.getDashboardRoutes().map(this.getMenuItem.bind(this)); + + items.push(this.getCloseSessionItem()); + + return items; } getMenuItem(item) { @@ -47,14 +48,38 @@ class DashboardMenu extends React.Component { }; } + getCloseSessionItem() { + return { + content: i18n('CLOSE_SESSION'), + icon: 'lock' + } + } + getSelectedIndex() { let pathname = this.props.location.pathname; - return _.findIndex(dashboardRoutes, {path: pathname}); + return _.findIndex(this.getDashboardRoutes(), {path: pathname}); + } + + onItemClick(itemIndex) { + if (itemIndex < this.getDashboardRoutes().length) { + this.goToPathByIndex(itemIndex) + } else { + dispatch(SessionActions.logout()); + } } goToPathByIndex(itemIndex) { - this.context.router.push(dashboardRoutes[itemIndex].path); + this.context.router.push(this.getDashboardRoutes()[itemIndex].path); + } + + getDashboardRoutes() { + return [ + { path: '/dashboard', text: i18n('TICKET_LIST'), icon: 'file-text-o' }, + { path: '/dashboard/create-ticket', text: i18n('CREATE_TICKET'), icon: 'plus' }, + { path: '/dashboard/articles', text: i18n('VIEW_ARTICLES'), icon: 'book' }, + { path: '/dashboard/edit-profile', text: i18n('EDIT_PROFILE'), icon: 'pencil' } + ]; } } diff --git a/client/src/app/main/main-layout-header.js b/client/src/app/main/main-layout-header.js index de7138fe..43bc7e45 100644 --- a/client/src/app/main/main-layout-header.js +++ b/client/src/app/main/main-layout-header.js @@ -34,14 +34,14 @@ class MainLayoutHeader extends React.Component { if (this.props.session.logged) { result = ( -
- Welcome, John - +
+ {i18n('WELCOME')}, + {this.props.session.userName}
); } else { result = ( -
+
@@ -53,7 +53,7 @@ class MainLayoutHeader extends React.Component { getLanguageSelectorProps() { return { - className: 'main-layout-header--languages', + className: 'main-layout-header__languages', items: this.getLanguageList(), selectedIndex: Object.values(codeLanguages).indexOf(this.props.config.language), onChange: this.changeLanguage.bind(this) @@ -74,10 +74,6 @@ class MainLayoutHeader extends React.Component { this.props.dispatch(ConfigActions.changeLanguage(codeLanguages[language])); } - - logout() { - this.props.dispatch(SessionActions.logout()); - } } export default connect((store) => { diff --git a/client/src/app/main/main-layout-header.scss b/client/src/app/main/main-layout-header.scss index fbb2feda..b4491020 100644 --- a/client/src/app/main/main-layout-header.scss +++ b/client/src/app/main/main-layout-header.scss @@ -6,7 +6,11 @@ height: 32px; width: 100%; - &--login-links { + &__user-name { + color: $primary-red; + } + + &__login-links { border-top-left-radius: 4px; color: white; display: inline-block; @@ -14,7 +18,7 @@ padding: 5px 20px 0 10px; } - &--languages { + &__languages { float: right; position: relative; top: 5px; diff --git a/client/src/core-components/menu.js b/client/src/core-components/menu.js index 9701a899..1a102038 100644 --- a/client/src/core-components/menu.js +++ b/client/src/core-components/menu.js @@ -15,7 +15,7 @@ class Menu extends React.Component { icon: React.PropTypes.string })).isRequired, selectedIndex: React.PropTypes.number, - tabbable: React.PropTypes.boolean + tabbable: React.PropTypes.bool }; static defaultProps = { diff --git a/client/src/data/fixtures/user-fixtures.js b/client/src/data/fixtures/user-fixtures.js index d84f061a..e661d46c 100644 --- a/client/src/data/fixtures/user-fixtures.js +++ b/client/src/data/fixtures/user-fixtures.js @@ -103,5 +103,107 @@ module.exports = [ }; } } + }, + { + path: '/user/get', + time: 100, + response: function () { + return { + status: 'success', + data: { + name: 'Haskell Curry', + email: 'haskell@lambda.com', + tickets: [ + { + ticketNumber: '445441', + title: 'Problem with installation', + content: 'I had a problem with the installation of the php server', + department: { + id: 2, + name: 'Environment Setup' + }, + date: '15 Apr 2016', + file: 'http://www.opensupports.com/some_file.zip', + language: 'en', + unread: true, + closed: false, + author: { + id: 12, + name: 'Haskell Curry', + email: 'haskell@lambda.com' + }, + owner: { + id: 15, + name: 'Steve Jobs', + email: 'steve@jobs.com' + }, + comments: [ + { + content: 'Do you have apache installed? It generally happens if you dont have apache.', + author: { + id: 15, + name: 'Steve Jobs', + email: 'jobs@steve.com', + staff: true + }, + date: '12 Dec 2016', + file: '' + }, + { + content: 'I have already installed apache, but the problem persists', + author: { + id: 12, + name: 'Haskell Curry', + steve: 'haskell@lambda.com', + staff: false + }, + date: '12 Dec 2016', + file: '' + } + ] + }, + { + ticketNumber: '878552', + title: 'Lorem ipsum door', + content: 'I had a problem with the installation of the php server', + department: { + id: 2, + name: 'Environment Setup' + }, + date: '15 Apr 2016', + file: 'http://www.opensupports.com/some_file.zip', + language: 'en', + unread: false, + closed: false, + author: { + name: 'Haskell Curry', + email: 'haskell@lambda.com' + }, + owner: { + name: 'Steve Jobs' + }, + comments: [ + { + content: 'Do you have apache installed? It generally happens if you dont have apache.', + author: { + name: 'Steve Jobs', + email: 'jobs@steve.com', + staff: true + } + }, + { + content: 'I have already installed apache, but the problem persists', + author: { + name: 'Haskell Curry', + steve: 'haskell@lambda.com', + staff: false + } + } + ] + } + ] + } + }; + } } -]; +]; \ No newline at end of file diff --git a/client/src/data/languages/en.js b/client/src/data/languages/en.js index 6a56b4ac..acd007f8 100644 --- a/client/src/data/languages/en.js +++ b/client/src/data/languages/en.js @@ -1,4 +1,5 @@ export default { + 'WELCOME': 'Welcome', 'SUBMIT': 'Submit', 'LOG_IN': 'Log in', 'SIGN_UP': 'Sign up', @@ -8,6 +9,11 @@ export default { 'NEW_PASSWORD': 'New password', 'REPEAT_NEW_PASSWORD': 'Repeat new password', 'BACK_LOGIN_FORM': 'Back to login form', + 'TICKET_LIST': 'Ticket List', + 'CREATE_TICKET': 'Create Ticket', + 'VIEW_ARTICLES': 'View Articles', + 'EDIT_PROFILE': 'Edit Profile', + 'CLOSE_SESSION': 'Close session', //ERRORS 'EMAIL_NOT_EXIST': 'Email does not exist', diff --git a/client/src/lib-app/__mocks__/api-call-mock.js b/client/src/lib-app/__mocks__/api-call-mock.js index f7de46f3..3500f061 100644 --- a/client/src/lib-app/__mocks__/api-call-mock.js +++ b/client/src/lib-app/__mocks__/api-call-mock.js @@ -1,5 +1,8 @@ export default { call: stub().returns(new Promise(function (resolve) { - resolve(); + resolve({ + status: 'success', + data: {} + }); })) }; \ No newline at end of file diff --git a/client/src/lib-app/session-store.js b/client/src/lib-app/session-store.js index a243c9fd..fa635f4e 100644 --- a/client/src/lib-app/session-store.js +++ b/client/src/lib-app/session-store.js @@ -29,6 +29,17 @@ class SessionStore { closeSession() { this.removeItem('userId'); this.removeItem('token'); + + this.clearRememberData(); + this.clearUserData(); + } + + storeUserData(data) { + this.setItem('userData', JSON.stringify(data)); + } + + getUserData() { + return JSON.parse(this.getItem('userData')); } storeRememberData({token, userId, expiration}) { @@ -73,6 +84,10 @@ class SessionStore { this.removeItem('rememberData-expiration'); } + clearUserData() { + this.removeItem('userData'); + } + getItem(key) { return this.storage.getItem(key); } diff --git a/client/src/reducers/session-reducer.js b/client/src/reducers/session-reducer.js index 42dbe9e2..877bb590 100644 --- a/client/src/reducers/session-reducer.js +++ b/client/src/reducers/session-reducer.js @@ -19,8 +19,9 @@ class SessionReducer extends Reducer { 'LOGIN_FULFILLED': this.onLoginCompleted.bind(this), 'LOGIN_REJECTED': this.onLoginFailed, 'LOGOUT_FULFILLED': this.onLogout, + 'USER_DATA_FULFILLED': this.onUserDataRetrieved, 'CHECK_SESSION_REJECTED': (state) => { return _.extend({}, state, {initDone: true})}, - 'SESSION_CHECKED': (state) => { return _.extend({}, state, {initDone: true, logged: true})}, + 'SESSION_CHECKED': this.onSessionChecked, 'LOGIN_AUTO_FULFILLED': this.onAutoLogin.bind(this), 'LOGIN_AUTO_REJECTED': this.onAutoLoginFail }; @@ -54,7 +55,6 @@ class SessionReducer extends Reducer { onLogout(state) { sessionStore.closeSession(); - sessionStore.clearRememberData(); return _.extend({}, state, { initDone: true, @@ -77,7 +77,6 @@ class SessionReducer extends Reducer { onAutoLoginFail(state) { sessionStore.closeSession(); - sessionStore.clearRememberData(); return _.extend({}, state, { initDone: true @@ -95,6 +94,30 @@ class SessionReducer extends Reducer { sessionStore.createSession(resultData.userId, resultData.token); } + + onUserDataRetrieved(state, payload) { + let userData = payload.data; + + sessionStore.storeUserData(payload.data); + + return _.extend({}, state, { + userName: userData.name, + userEmail: userData.email, + userTickets: userData.tickets + }); + } + + onSessionChecked(state) { + let userData = sessionStore.getUserData(); + + return _.extend({}, state, { + initDone: true, + logged: true, + userName: userData.name, + userEmail: userData.email, + userTickets: userData.tickets + }); + } } export default SessionReducer.getInstance(); \ No newline at end of file