Browse Source

connected login and logout on real api + integration of flash messages

Skylsmoi 6 years ago
parent
commit
67cebc1baf

+ 2 - 2
jsonserver/server.js View File

@@ -40,14 +40,14 @@ server.patch('/user', (req, res) => res.jsonp({lang: 'fr'}))
40 40
 
41 41
 server.get('/lang', (req, res) => res.jsonp(jsonDb.lang))
42 42
 
43
-server.post('/user/login', (req, res) => {
43
+server.post('/sessions/login', (req, res) => {
44 44
   if (req.body.login !== '' && req.body.password !== '') return res.jsonp(jsonDb.user_logged)
45 45
   else return res.jsonp('error')
46 46
 })
47 47
 
48 48
 server.get('/app/config', (req, res) => res.jsonp(jsonDb.app_config))
49 49
 
50
-server.get('/user/is_logged_in', (req, res) =>
50
+server.get('/sessions/whoami', (req, res) =>
51 51
   // res.jsonp({"logged": false})
52 52
   res.jsonp(jsonDb.user_logged)
53 53
 )

+ 12 - 11
jsonserver/static_db.json View File

@@ -11,17 +11,18 @@
11 11
     "active": false
12 12
   }],
13 13
   "user_logged": {
14
-    "logged": true,
15
-    "user": {
16
-      "id": 5,
17
-      "username": "franclecompte",
18
-      "firstname": "François",
19
-      "lastname": "Lecompte",
20
-      "email": "francois.lecompte@algoo.fr",
21
-      "avatar": "https://rawgit.com/tracim/tracim_front/develop/src/img/imgProfil.png",
22
-      "role": "Utilisateur",
23
-      "caldav_url": "http://algoo.trac.im/caldav/user/5.ics/"
24
-    }
14
+    "timezone": "",
15
+    "profile": {
16
+      "id": 3,
17
+      "slug": "administrators"
18
+    },
19
+    "email": "admin@admin.admin",
20
+    "is_active": true,
21
+    "caldav_url": null,
22
+    "user_id": 1,
23
+    "avatar_url": null,
24
+    "created": "2018-05-17T08:43:03.745219+00:00",
25
+    "display_name": "Global manager"
25 26
   },
26 27
   "app_config": [{
27 28
     "name": "PageHtml",

+ 19 - 11
src/action-creator.async.js View File

@@ -5,10 +5,10 @@ import {
5 5
   LANG,
6 6
   updateLangList,
7 7
   USER_LOGIN,
8
+  USER_LOGOUT,
8 9
   USER_DATA,
9 10
   USER_ROLE,
10 11
   USER_CONNECTED,
11
-  setUserConnected,
12 12
   updateUserData,
13 13
   setUserRole,
14 14
   WORKSPACE,
@@ -89,33 +89,41 @@ export const getTimezone = () => async dispatch => {
89 89
 }
90 90
 
91 91
 export const postUserLogin = (login, password, rememberMe) => async dispatch => {
92
-  const fetchUserLogin = await fetchWrapper({
93
-    url: `${FETCH_CONFIG.apiUrl}/sessions/login`,
92
+  return fetchWrapper({
93
+    url: `${FETCH_CONFIG.mockApiUrl}/sessions/login`,
94 94
     param: {
95 95
       headers: {...FETCH_CONFIG.headers},
96 96
       method: 'POST',
97 97
       body: JSON.stringify({
98 98
         email: login,
99
-        password: password
100
-        // remember_me: rememberMe
99
+        password: password,
100
+        remember_me: rememberMe
101 101
       })
102 102
     },
103 103
     actionName: USER_LOGIN,
104 104
     dispatch
105 105
   })
106
-  if (fetchUserLogin.status === 200) dispatch(setUserConnected(fetchUserLogin.json))
106
+}
107
+
108
+export const postUserLogout = () => async dispatch => {
109
+  return fetchWrapper({
110
+    url: `${FETCH_CONFIG.mockApiUrl}/sessions/logout`,
111
+    param: {
112
+      headers: {...FETCH_CONFIG.headers},
113
+      method: 'POST'
114
+    },
115
+    actionName: USER_LOGOUT,
116
+    dispatch
117
+  })
107 118
 }
108 119
 
109 120
 export const getUserIsConnected = () => async dispatch => {
110
-  const fetchUserLogged = await fetchWrapper({
111
-    url: `${FETCH_CONFIG.apiUrl}/sessions/whoami`,
121
+  return fetchWrapper({
122
+    url: `${FETCH_CONFIG.mockApiUrl}/sessions/whoami`,
112 123
     param: {...FETCH_CONFIG.header, method: 'GET'},
113 124
     actionName: USER_CONNECTED,
114 125
     dispatch
115 126
   })
116
-  if (fetchUserLogged.status === 200) dispatch(setUserConnected(fetchUserLogged.json))
117
-  else if (fetchUserLogged.status === 401) dispatch(setUserConnected({logged: false}))
118
-  else dispatch(setUserConnected({logged: undefined}))
119 127
 }
120 128
 
121 129
 export const getUserRole = user => async dispatch => {

+ 12 - 1
src/action-creator.sync.js View File

@@ -1,11 +1,22 @@
1 1
 export const TIMEZONE = 'Timezone'
2 2
 export const setTimezone = timezone => ({ type: `Set/${TIMEZONE}`, timezone })
3 3
 
4
+export const FLASH_MESSAGE = 'FlashMessage'
5
+export const newFlashMessage = (msgText = '', msgType = 'info', msgDelay = 5000) => dispatch => {
6
+  msgDelay !== 0 && window.setTimeout(() => dispatch(removeFlashMessage(msgText)), msgDelay)
7
+  return dispatch(addFlashMessage({message: msgText, type: msgType}))
8
+}
9
+export const addFlashMessage = msg => ({ type: `Add/${FLASH_MESSAGE}`, msg })
10
+export const removeFlashMessage = msg => ({ type: `Remove/${FLASH_MESSAGE}`, msg })
11
+
4 12
 export const USER_LOGIN = 'User/Login'
13
+export const USER_LOGOUT = 'User/Logout'
5 14
 export const USER_DATA = 'User/Data'
6 15
 export const USER_ROLE = 'User/Role'
7 16
 export const USER_CONNECTED = 'User/Connected'
8
-export const setUserConnected = data => ({ type: `Set/${USER_CONNECTED}`, data })
17
+export const USER_DISCONNECTED = 'User/Disconnected'
18
+export const setUserConnected = user => ({ type: `Set/${USER_CONNECTED}`, user })
19
+export const setUserDisconnected = () => ({ type: `Set/${USER_DISCONNECTED}` })
9 20
 export const updateUserData = userData => ({ type: `Update/${USER_DATA}`, data: userData })
10 21
 export const setUserRole = userRole => ({ type: `Set/${USER_ROLE}`, userRole }) // this actually update workspaceList state
11 22
 export const updateUserWorkspaceSubscriptionNotif = (workspaceId, subscriptionNotif) =>

+ 1 - 1
src/appFactory.js View File

@@ -4,7 +4,7 @@ import { FETCH_CONFIG } from './helper.js'
4 4
 export function appFactory (WrappedComponent) {
5 5
   return class AppFactory extends React.Component {
6 6
     renderApp = (appConfig, user, content) => GLOBAL_renderApp({
7
-      loggedUser: user.isLoggedIn ? user : {},
7
+      loggedUser: user.logged ? user : {},
8 8
       config: {
9 9
         ...appConfig,
10 10
         apiUrl: FETCH_CONFIG.apiUrl

src/container/FlashMessage.jsx → src/component/FlashMessage.jsx View File

@@ -1,38 +1,37 @@
1
-import React, { Component } from 'react'
1
+import React from 'react'
2
+import classnames from 'classnames'
2 3
 
3
-class FlashMessage extends Component {
4
-  render() {
5
-    return(
6
-      <div className='flashmessage'>
4
+const FlashMessage = props => {
5
+  return (
6
+    <div className='flashmessage'>
7
+      {props.flashMessage.length > 0 && (
7 8
         <div className='flashmessage__container card'>
8
-          <div className='flashmessage__container__header' />
9
+          <div className={classnames('flashmessage__container__header', props.flashMessage[0].type)} />
9 10
 
10 11
           <div className='card-body nopadding'>
11
-
12
-            <div className='flashmessage__container__close'>
12
+            <div className='flashmessage__container__close' onClick={() => props.removeFlashMessage(props.flashMessage[0].message)}>
13 13
               <i className='fa fa-times' />
14 14
             </div>
15 15
 
16 16
             <div className='flashmessage__container__content'>
17
-              <div className='flashmessage__container__content__icon'>
17
+              <div className={classnames('flashmessage__container__content__icon', props.flashMessage[0].type)}>
18 18
                 <i className='fa fa-times-circle' />
19 19
               </div>
20 20
 
21 21
               <div className='flashmessage__container__content__text'>
22 22
                 <div className='flashmessage__container__content__text__title'>
23
-                  Sorry !
23
+                  {props.t('FlashMessage.error')}
24 24
                 </div>
25 25
                 <div className='flashmessage__container__content__text__paragraph'>
26
-                  Reprehenderit reprehenderit veniam dolore velit dolor velit in occaecat dolor veniam nisi officia velit consequat amet cupidatat.
27
-                  Reprehenderit reprehenderit veniam dolore velit dolor velit in occaecat dolor veniam nisi officia velit consequat amet cupidatat.
26
+                  {props.flashMessage[0].message}
28 27
                 </div>
29 28
               </div>
30 29
             </div>
31 30
           </div>
32 31
         </div>
33
-      </div>
34
-    )
35
-  }
32
+      )}
33
+    </div>
34
+  )
36 35
 }
37 36
 
38 37
 export default FlashMessage

+ 2 - 2
src/component/Header/MenuActionListItem/MenuProfil.jsx View File

@@ -4,10 +4,10 @@ import PropTypes from 'prop-types'
4 4
 import { PAGE_NAME } from '../../../helper.js'
5 5
 
6 6
 const MenuProfil = props => {
7
-  return props.user.isLoggedIn
7
+  return props.user.logged
8 8
     ? (
9 9
       <li className='header__menu__rightside__itemprofil'>
10
-        <div className='header__menu__rightside__itemprofil__profilgroup dropdown'>
10
+        <div className='profilgroup dropdown'>
11 11
           <button className='profilgroup__name btn btn-secondary dropdown-toggle' type='button' id='dropdownMenuButton' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'>
12 12
             <img className='profilgroup__name__imgprofil' src={props.user.avatar} alt='avatar' />
13 13
             <div className='profilgroup__name__text'>

+ 3 - 1
src/component/common/Input/InputGroupText.jsx View File

@@ -10,7 +10,7 @@ const InputGroupText = props => {
10 10
       </div>
11 11
       <input
12 12
         type={props.type}
13
-        className={classnames(`${props.parentClassName}__input`, 'form-control')}
13
+        className={classnames(`${props.parentClassName}__input`, 'form-control', {'is-invalid': props.isInvalid})}
14 14
         placeholder={props.placeHolder}
15 15
         value={props.value}
16 16
         onChange={props.onChange}
@@ -32,6 +32,7 @@ InputGroupText.propTypes = {
32 32
   icon: PropTypes.string,
33 33
   placeHolder: PropTypes.string,
34 34
   invalidMsg: PropTypes.string,
35
+  isInvalid: PropTypes.bool,
35 36
   onChange: PropTypes.func
36 37
 }
37 38
 
@@ -40,5 +41,6 @@ InputGroupText.defaultProps = {
40 41
   icon: false,
41 42
   placeHolder: '',
42 43
   invalidMsg: false,
44
+  isInvalid: false,
43 45
   onChange: () => {}
44 46
 }

+ 17 - 4
src/container/Header.jsx View File

@@ -1,6 +1,6 @@
1 1
 import React from 'react'
2 2
 import { connect } from 'react-redux'
3
-import i18n from '../i18n.js'
3
+import { translate } from 'react-i18next'
4 4
 import Logo from '../component/Header/Logo.jsx'
5 5
 import NavbarToggler from '../component/Header/NavbarToggler.jsx'
6 6
 import MenuLinkList from '../component/Header/MenuLinkList.jsx'
@@ -10,7 +10,14 @@ import MenuActionListItemDropdownLang from '../component/Header/MenuActionListIt
10 10
 import MenuActionListItemHelp from '../component/Header/MenuActionListItem/Help.jsx'
11 11
 import MenuActionListItemMenuProfil from '../component/Header/MenuActionListItem/MenuProfil.jsx'
12 12
 import logoHeader from '../img/logo-tracim.png'
13
-import { setLangActive } from '../action-creator.sync.js'
13
+import {
14
+  newFlashMessage,
15
+  setLangActive,
16
+  setUserDisconnected
17
+} from '../action-creator.sync.js'
18
+import {
19
+  postUserLogout
20
+} from '../action-creator.async.js'
14 21
 
15 22
 class Header extends React.Component {
16 23
   handleClickLogo = () => {}
@@ -29,7 +36,13 @@ class Header extends React.Component {
29 36
 
30 37
   handleClickHelp = () => {}
31 38
 
32
-  handleClickLogout = () => {}
39
+  handleClickLogout = async () => {
40
+    const { dispatch, t } = this.props
41
+
42
+    const fetchPostUserLogout = await dispatch(postUserLogout())
43
+    if (fetchPostUserLogout.status === 204) dispatch(setUserDisconnected())
44
+    else dispatch(newFlashMessage(t('Login.logout_error', 'danger')))
45
+  }
33 46
 
34 47
   render () {
35 48
     const { lang, user } = this.props
@@ -79,4 +92,4 @@ class Header extends React.Component {
79 92
 }
80 93
 
81 94
 const mapStateToProps = ({ lang, user }) => ({ lang, user })
82
-export default connect(mapStateToProps)(Header)
95
+export default connect(mapStateToProps)(translate()(Header))

+ 1 - 1
src/container/Home.jsx View File

@@ -7,7 +7,7 @@ class Home extends React.Component {
7 7
     return (
8 8
       <div>
9 9
         Home.<br />
10
-        User logged in : {user.isLoggedIn.toString()}
10
+        User logged in : {user.logged.toString()}
11 11
       </div>
12 12
     )
13 13
   }

+ 31 - 11
src/container/Login.jsx View File

@@ -1,6 +1,7 @@
1 1
 import React from 'react'
2 2
 import { connect } from 'react-redux'
3 3
 import { Redirect } from 'react-router'
4
+import { translate } from 'react-i18next'
4 5
 import LoginLogo from '../component/Login/LoginLogo.jsx'
5 6
 import LoginLogoImg from '../img/logoTracimWhite.svg'
6 7
 import { postUserLogin } from '../action-creator.async.js'
@@ -11,31 +12,48 @@ import InputGroupText from '../component/common/Input/InputGroupText.jsx'
11 12
 import InputCheckbox from '../component/common/Input/InputCheckbox.jsx'
12 13
 import Button from '../component/common/Input/Button.jsx'
13 14
 import LoginBtnForgotPw from '../component/Login/LoginBtnForgotPw.jsx'
15
+import {
16
+  newFlashMessage,
17
+  setUserConnected
18
+} from '../action-creator.sync.js'
19
+import {PAGE_NAME} from '../helper.js'
14 20
 
15 21
 class Login extends React.Component {
16 22
   constructor (props) {
17 23
     super(props)
18 24
     this.state = {
19
-      inputLogin: '',
20
-      inputPassword: '',
25
+      inputLogin: {
26
+        value: '',
27
+        isInvalid: false
28
+      },
29
+      inputPassword: {
30
+        value: '',
31
+        isInvalid: false
32
+      },
21 33
       inputRememberMe: false
22 34
     }
23 35
   }
24 36
 
25
-  handleChangeLogin = e => this.setState({inputLogin: e.target.value})
26
-  handleChangePassword = e => this.setState({inputPassword: e.target.value})
37
+  handleChangeLogin = e => this.setState({inputLogin: {...this.state.inputLogin, value: e.target.value}})
38
+  handleChangePassword = e => this.setState({inputPassword: {...this.state.inputPassword, value: e.target.value}})
27 39
   handleChangeRememberMe = () => this.setState(prev => ({inputRememberMe: !prev.inputRememberMe}))
28 40
 
29 41
   handleClickSubmit = async () => {
30
-    const { history, dispatch } = this.props
42
+    const { history, dispatch, t } = this.props
31 43
     const { inputLogin, inputPassword, inputRememberMe } = this.state
32 44
 
33
-    await dispatch(postUserLogin(inputLogin, inputPassword, inputRememberMe))
34
-    history.push('/')
45
+    const fetchPostUserLogin = await dispatch(postUserLogin(inputLogin.value, inputPassword.value, inputRememberMe))
46
+
47
+    if (fetchPostUserLogin.status === 200) {
48
+      dispatch(setUserConnected({...fetchPostUserLogin.json, logged: true}))
49
+      history.push(PAGE_NAME.HOME)
50
+    } else if (fetchPostUserLogin.status === 400) {
51
+      dispatch(newFlashMessage(t('Login.fail'), 'danger'))
52
+    }
35 53
   }
36 54
 
37 55
   render () {
38
-    if (this.props.user.isLoggedIn) return <Redirect to={{pathname: '/'}} />
56
+    if (this.props.user.logged) return <Redirect to={{pathname: '/'}} />
39 57
     else {
40 58
       return (
41 59
         <section className='loginpage'>
@@ -58,7 +76,8 @@ class Login extends React.Component {
58 76
                         type='email'
59 77
                         placeHolder='Adresse Email'
60 78
                         invalidMsg='Email invalide.'
61
-                        value={this.state.inputLogin}
79
+                        isInvalid={this.state.inputLogin.isInvalid}
80
+                        value={this.state.inputLogin.value}
62 81
                         onChange={this.handleChangeLogin}
63 82
                       />
64 83
 
@@ -69,7 +88,8 @@ class Login extends React.Component {
69 88
                         type='password'
70 89
                         placeHolder='Mot de passe'
71 90
                         invalidMsg='Mot de passe invalide.'
72
-                        value={this.state.inputPassword}
91
+                        isInvalid={this.state.inputPassword.isInvalid}
92
+                        value={this.state.inputPassword.value}
73 93
                         onChange={this.handleChangePassword}
74 94
                       />
75 95
 
@@ -114,4 +134,4 @@ class Login extends React.Component {
114 134
 }
115 135
 
116 136
 const mapStateToProps = ({ user }) => ({ user })
117
-export default connect(mapStateToProps)(Login)
137
+export default connect(mapStateToProps)(translate()(Login))

+ 2 - 2
src/container/PrivateRoute.jsx View File

@@ -8,7 +8,7 @@ import { Route, Redirect, withRouter } from 'react-router-dom'
8 8
 // /!\ you shall destruct props.component otherwise you get a warning:
9 9
 // "You should not use <Route component> and <Route render> in the same route; <Route render> will be ignored
10 10
 const PrivateRoute = ({ component: Component, ...rest }) => (
11
-  <Route {...rest} render={props => rest.user.isLoggedIn
11
+  <Route {...rest} render={props => rest.user.logged
12 12
     ? <Component {...props} />
13 13
     : <Redirect to={{pathname: '/login', state: {from: props.location}}} />
14 14
   } />
@@ -20,6 +20,6 @@ export default withRouter(connect(mapStateToProps)(PrivateRoute))
20 20
 PrivateRoute.propTypes = {
21 21
   component: PropTypes.func.isRequired,
22 22
   user: PropTypes.shape({ // user is get with redux connect
23
-    isLoggedIn: PropTypes.bool.isRequired
23
+    logged: PropTypes.bool.isRequired
24 24
   })
25 25
 }

+ 26 - 8
src/container/Tracim.jsx View File

@@ -1,11 +1,12 @@
1 1
 import React from 'react'
2 2
 import { connect } from 'react-redux'
3
+import { translate } from 'react-i18next'
3 4
 import Footer from '../component/Footer.jsx'
4 5
 import Header from './Header.jsx'
5 6
 import Login from './Login.jsx'
6 7
 import Dashboard from './Dashboard.jsx'
7 8
 import Account from './Account.jsx'
8
-// import FlashMessage from './FlashMessage.jsx'
9
+import FlashMessage from '../component/FlashMessage.jsx'
9 10
 import WorkspaceContent from './WorkspaceContent.jsx'
10 11
 import {
11 12
   Route,
@@ -17,21 +18,38 @@ import {
17 18
   getLangList,
18 19
   getUserIsConnected
19 20
 } from '../action-creator.async.js'
21
+import {
22
+  removeFlashMessage, setUserConnected
23
+} from '../action-creator.sync.js'
20 24
 
21 25
 class Tracim extends React.Component {
22
-  componentDidMount () {
23
-    this.props.dispatch(getUserIsConnected())
24
-    this.props.dispatch(getLangList())
26
+  async componentDidMount () {
27
+    const { dispatch } = this.props
28
+
29
+    dispatch(getLangList())
30
+
31
+    const fetchGetUserIsConnected = await dispatch(getUserIsConnected())
32
+    switch (fetchGetUserIsConnected.status) {
33
+      case 200:
34
+        dispatch(setUserConnected({...fetchGetUserIsConnected.json, logged: true})); break
35
+      case 401:
36
+        dispatch(setUserConnected({logged: false})); break
37
+      default:
38
+        dispatch(setUserConnected({logged: undefined})); break
39
+    }
25 40
   }
26 41
 
42
+  handleRemoveFlashMessage = msg => this.props.dispatch(removeFlashMessage(msg))
43
+
27 44
   render () {
28
-    const { user } = this.props
45
+    const { flashMessage, user, t } = this.props
29 46
 
30 47
     return (
31 48
       <div className='tracim'>
32 49
         <Header />
50
+        <FlashMessage flashMessage={flashMessage} removeFlashMessage={this.handleRemoveFlashMessage} t={t} />
33 51
 
34
-        { user.isLoggedIn === undefined
52
+        { user.logged === undefined
35 53
           ? (<div />) // while we dont know if user is connected, display nothing but the header @TODO show loader
36 54
           : (
37 55
             <div className='tracim__content'>
@@ -51,5 +69,5 @@ class Tracim extends React.Component {
51 69
   }
52 70
 }
53 71
 
54
-const mapStateToProps = ({ user }) => ({ user })
55
-export default withRouter(connect(mapStateToProps)(Tracim))
72
+const mapStateToProps = ({ flashMessage, user }) => ({ flashMessage, user })
73
+export default withRouter(connect(mapStateToProps)(translate()(Tracim)))

+ 16 - 7
src/css/FlashMessage.styl View File

@@ -3,9 +3,9 @@
3 3
   display flex
4 4
   justify-content center
5 5
   width 100%
6
-  z-index 3
6
+  z-index 10
7 7
   &__container
8
-    margin-top 120px
8
+    margin-top 50px
9 9
     border 0
10 10
     border-radius 10px
11 11
     width 800px
@@ -16,7 +16,12 @@
16 16
       border-top-left-radius 10px
17 17
       width 100%
18 18
       height 8px
19
-      background-color calendar
19
+      &.success
20
+        background-color success
21
+      &.info
22
+        background-color info
23
+      &.danger
24
+        background-color danger
20 25
     &__close
21 26
       display flex
22 27
       justify-content flex-end
@@ -25,14 +30,18 @@
25 30
     &__content
26 31
       display flex
27 32
       align-items center
28
-      margin 10px 0 35px 0
33
+      margin 10px 0 25px 0
29 34
       &__icon
30 35
         padding 0 30px
31
-        font-size 60px
32
-        color calendar
36
+        font-size 40px
37
+        &.success
38
+          color success
39
+        &.info
40
+          color info
41
+        &.danger
42
+          color danger
33 43
       &__text
34 44
         &__title
35
-          margin-bottom 10px
36 45
           font-size 20px
37 46
           font-weight 600
38 47
           color calendar

+ 1 - 0
src/css/Generic.styl View File

@@ -109,6 +109,7 @@
109 109
     cursor pointer
110 110
     .setting__link
111 111
       padding 10px
112
+      cursor pointer
112 113
 
113 114
 .fa-file-image-o
114 115
   color previewColor

+ 6 - 4
src/css/Header.styl View File

@@ -68,11 +68,12 @@
68 68
           .btnquestion__icon
69 69
             color darkGrey
70 70
       &__itemprofil
71
-        &__profilgroup
72
-          .profilgroup__name
71
+        .profilgroup
72
+          &__name
73 73
             border 0
74 74
             padding 0 5px
75 75
             background-color transparent
76
+            cursor pointer
76 77
             &:hover
77 78
               background-color lightGrey
78 79
             &:focus
@@ -91,15 +92,16 @@
91 92
               vertical-align middle
92 93
               margin-right 10px
93 94
               color darkGrey
94
-          .profilgroup__sub
95
+          &__sub
95 96
             color darkGrey
96 97
             cursor pointer
97 98
             &:focus
98 99
               box-shadow none
99
-          .profilgroup__setting
100
+          &__setting
100 101
             padding 0
101 102
             left inherit
102 103
             right 0
104
+            cursor pointer
103 105
             .dropdown-item
104 106
               color fontColor
105 107
             &__link

+ 5 - 0
src/css/Variable.styl View File

@@ -70,6 +70,11 @@ validateColor = explore
70 70
 cancelColor = red
71 71
 outdateColor = grey
72 72
 
73
+/** Flash Message Color **/
74
+success = #30ce4e
75
+info = #0094bc
76
+danger = #e81a32
77
+
73 78
 /*************************/
74 79
 /**** BOX SHADOW ****/
75 80
 shadow-bottom = 0px 0px 5px 1px #606060

+ 19 - 0
src/reducer/flashMessage.js View File

@@ -0,0 +1,19 @@
1
+import {
2
+  FLASH_MESSAGE
3
+} from '../action-creator.sync.js'
4
+
5
+export default function flashMessage (state = [], action) {
6
+  switch (action.type) {
7
+    case `Add/${FLASH_MESSAGE}`:
8
+      return [...state, {
9
+        message: action.msg.message,
10
+        type: action.msg.type || 'info' // may be info, success, danger
11
+      }]
12
+
13
+    case `Remove/${FLASH_MESSAGE}`:
14
+      return state.filter(fm => fm.message === action.message)
15
+
16
+    default:
17
+      return state
18
+  }
19
+}

+ 2 - 1
src/reducer/root.js View File

@@ -1,5 +1,6 @@
1 1
 import { combineReducers } from 'redux'
2 2
 import lang from './lang.js'
3
+import flashMessage from './flashMessage.js'
3 4
 import user from './user.js'
4 5
 import workspace from './workspace.js'
5 6
 import workspaceList from './workspaceList.js'
@@ -7,6 +8,6 @@ import activeFileContent from './activeFileContent.js'
7 8
 import app from './app.js'
8 9
 import timezone from './timezone.js'
9 10
 
10
-const rootReducer = combineReducers({ lang, user, workspace, workspaceList, activeFileContent, app, timezone })
11
+const rootReducer = combineReducers({ lang, flashMessage, user, workspace, workspaceList, activeFileContent, app, timezone })
11 12
 
12 13
 export default rootReducer

+ 17 - 23
src/reducer/user.js View File

@@ -1,38 +1,32 @@
1 1
 import {
2 2
   USER_CONNECTED,
3
+  USER_DISCONNECTED,
3 4
   USER_DATA
4 5
 } from '../action-creator.sync.js'
5 6
 
6
-const serializeUser = data => ({
7
-  id: data.user.id,
8
-  isLoggedIn: data.logged,
9
-  username: data.user.username,
10
-  firstname: data.user.firstname,
11
-  lastname: data.user.lastname,
12
-  email: data.user.email,
13
-  avatar: data.user.avatar,
14
-  role: data.user.role,
15
-  job: data.user.job,
16
-  company: data.user.company,
17
-  caldavUrl: data.user.caldav_url
18
-})
19
-
20 7
 const defaultUser = {
21
-  id: -1,
22
-  isLoggedIn: undefined, // undefined means the api (/is_logged_in) has not responded yet
23
-  username: '',
24
-  firstname: '',
25
-  lastname: '',
8
+  user_id: -1,
9
+  logged: false,
10
+  timezone: '',
11
+  profile: {
12
+    id: 1,
13
+    slug: 'user'
14
+  },
26 15
   email: '',
27
-  avatar: ''
16
+  is_active: true,
17
+  caldav_url: null,
18
+  avatar_url: null,
19
+  created: '',
20
+  display_name: ''
28 21
 }
29 22
 
30 23
 export default function user (state = defaultUser, action) {
31 24
   switch (action.type) {
32 25
     case `Set/${USER_CONNECTED}`:
33
-      return action.data.logged
34
-        ? serializeUser(action.data)
35
-        : {...defaultUser, isLoggedIn: false}
26
+      return action.user
27
+
28
+    case `Set/${USER_DISCONNECTED}`:
29
+      return defaultUser
36 30
 
37 31
     case `Update/${USER_DATA}`:
38 32
       return {...state, ...action.data}

+ 7 - 0
src/translate/en.js View File

@@ -7,6 +7,13 @@ const en = {
7 7
       marketing_msg: 'Create your own collaborative workspaces on trac.im',
8 8
       copyright: 'Copyright 2013 - 2017'
9 9
     },
10
+    FlashMessage: {
11
+      error: 'Error'
12
+    },
13
+    Login: {
14
+      fail: 'Unknown email or password',
15
+      logout_error: 'Disconnection error'
16
+    },
10 17
     FileItemHeader: {
11 18
       type: 'Type',
12 19
       document_name: "Document's name",

+ 7 - 0
src/translate/fr.js View File

@@ -7,6 +7,13 @@ const fr = {
7 7
       marketing_msg: 'Créer votre propre espace de travail collaboratif sur trac.im',
8 8
       copyright: 'Copyright 2013 - 2017'
9 9
     },
10
+    FlashMessage: {
11
+      error: 'Erreur'
12
+    },
13
+    Login: {
14
+      fail: 'Email ou mot de passe inconnu.',
15
+      logout_error: 'Erreur de déconnexion.'
16
+    },
10 17
     FileItemHeader: {
11 18
       type: 'Type',
12 19
       document_name: 'Nom du document',