add menu to change current group and create a new group
authorymh <ymh.work@gmail.com>
Thu, 03 Aug 2017 17:33:00 +0200
changeset 134 be36eed5e6e0
parent 133 6f3078f7fd47
child 135 b5aafa462956
add menu to change current group and create a new group
client/src/actions/groupActions.js
client/src/api/APIClient.js
client/src/components/CreateGroup.js
client/src/components/GroupForm.js
client/src/components/Login.js
client/src/components/Navbar.js
client/src/components/SessionForm.js
client/src/constants/actionTypes.js
client/src/index.js
client/src/reducers/authReducer.js
client/src/reducers/index.js
client/src/sagas/NoteSyncronizer.js
client/src/sagas/SessionSyncronizer.js
client/src/sagas/authSaga.js
client/src/sagas/groupSaga.js
client/src/sagas/networkSaga.js
client/src/sagas/selectors.js
client/src/sagas/syncSaga.js
client/src/selectors/authSelectors.js
client/src/selectors/coreSelectors.js
client/src/selectors/syncSelectors.js
client/src/store/configureStore.js
--- a/client/src/actions/groupActions.js	Thu Aug 03 09:44:37 2017 +0200
+++ b/client/src/actions/groupActions.js	Thu Aug 03 17:33:00 2017 +0200
@@ -15,3 +15,16 @@
 
 export const groupLoadAsync = () =>
   ({ type: types.GROUP_LOAD_ASYNC });
+
+export const groupSetCurrent = (groupName) =>
+  ({ type: types.GROUP_SET_GROUP, group: groupName });
+
+export const createGroup = (name, description) => {
+  return {
+    type: types.GROUP_CREATE_ASYNC,
+    group: {
+      name,
+      description
+    }
+  };
+}
--- a/client/src/api/APIClient.js	Thu Aug 03 09:44:37 2017 +0200
+++ b/client/src/api/APIClient.js	Thu Aug 03 17:33:00 2017 +0200
@@ -1,4 +1,5 @@
 import qs from 'qs';
+import { getToken, getClientId } from '../selectors/authSelectors'
 
 class APIClient {
   constructor(baseURL) {
@@ -40,12 +41,12 @@
 
   getToken = () => {
     const state = this.store.getState();
-    return state.getIn(['authStatus', 'token']);
+    return getToken(state);
   }
 
   getClientId = () => {
     const state = this.store.getState();
-    return state.getIn(['authStatus', 'clientId']);
+    return getClientId(state);
   }
 
   hasToken = () => {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/CreateGroup.js	Thu Aug 03 17:33:00 2017 +0200
@@ -0,0 +1,124 @@
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+import { Grid, Row, Col, Panel, FormGroup, ControlLabel, FormControl, Button, Alert, HelpBlock } from 'react-bootstrap';
+import '../App.css';
+import Navbar from './Navbar';
+import * as authActions from '../actions/authActions';
+import { getOnline, getCreateGroup } from '../selectors/authSelectors';
+
+class CreateGroup extends Component {
+
+  state = {
+    name: '',
+    description: ''
+  }
+
+  createGroup = () => {
+
+    const { name, description } = this.state;
+
+    if(name && name.trim() !== "") {
+      this.props.authActions.createGroup(name, description);
+    }
+  }
+
+  submit = (e) => {
+    e.preventDefault();
+
+    this.createGroup();
+  }
+
+  handleInputChange = (e) => {
+    const target = e.target;
+    const value = target.value;
+    const name = target.name;
+
+    this.setState({
+      [name]: value
+    });
+  }
+
+  renderErrorMessage(errorMessages, fieldname) {
+    if (errorMessages && fieldname in errorMessages) {
+      return errorMessages[fieldname].map((message, key) =>
+        <HelpBlock key={ key }>{ message }</HelpBlock>
+      );
+    }
+  }
+
+  renderNonFieldErrors(errorMessages) {
+    if (errorMessages && 'non_field_errors' in errorMessages) {
+      const errors = errorMessages['non_field_errors'];
+      return (
+        <Alert bsStyle="danger">
+        { errors.map((message, key) =>
+          <p key={ key }>{ message }</p>
+        ) }
+        </Alert>
+      )
+    }
+  }
+
+  cancel = (e) => {
+    e.preventDefault();
+    this.props.history.push('/sessions');
+  }
+
+
+  render() {
+
+    const panelHeader = (
+      <h4 className="text-uppercase text-center">Créer un groupe</h4>
+    )
+
+    const errorMessages = this.props.createGroup.getIn(['errorMessages', 'data']);
+    const okDisabled = (!this.state.name || this.state.name.trim() === "");
+
+    return (
+      <div>
+        <Navbar history={this.props.history} />
+        <Grid fluid>
+          <Row>
+            <Col md={6} mdOffset={3}>
+              <Panel header={ panelHeader } className="panel-login">
+                <form onSubmit={this.submit}>
+                  <FormGroup validationState={ errorMessages && ('name' in errorMessages) ? 'error' : null }>
+                    <ControlLabel>Nom</ControlLabel>
+                    <FormControl componentClass="input" type="text" onChange={this.handleInputChange} name="name" placeholder="Nom du groupe..."/>
+                     { this.renderErrorMessage(errorMessages, 'name') }
+                  </FormGroup>
+                  <FormGroup validationState={ errorMessages && ('description' in errorMessages) ? 'error' : null }>
+                    <ControlLabel>Password</ControlLabel>
+                    <FormControl componentClass="textarea" onChange={this.handleInputChange} name="description" placeholder="Description..."/>
+                     { this.renderErrorMessage(errorMessages, 'description') }
+                  </FormGroup>
+                  { this.renderNonFieldErrors(errorMessages) }
+                  <Row>
+                  <Col md={6}><Button type="submit" block bsStyle="primary" disabled={okDisabled}>Ok</Button></Col>
+                  <Col md={6}><Button block bsStyle="default" onClick={this.cancel}>Annuler</Button></Col>
+                  </Row>
+                </form>
+              </Panel>
+            </Col>
+          </Row>
+        </Grid>
+      </div>
+    );
+  }
+}
+
+function mapStateToProps(state, props) {
+  return {
+    createGroup: getCreateGroup(state),
+    online: getOnline(state),
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  return {
+    authActions: bindActionCreators(authActions, dispatch),
+  }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(CreateGroup);
--- a/client/src/components/GroupForm.js	Thu Aug 03 09:44:37 2017 +0200
+++ b/client/src/components/GroupForm.js	Thu Aug 03 17:33:00 2017 +0200
@@ -4,6 +4,7 @@
 import { FormGroup, FormControl, Button, InputGroup, HelpBlock, Glyphicon } from 'react-bootstrap';
 import * as authActions from '../actions/authActions';
 import * as sessionsActions from '../actions/sessionsActions';
+import { getOnline, getCreateGroup } from '../selectors/authSelectors';
 
 class GroupForm extends Component {
 
@@ -91,8 +92,8 @@
 function mapStateToProps(state, props) {
 
   return {
-    createGroup: state.get('createGroup'),
-    online: state.getIn(['status', 'online']),
+    createGroup: getCreateGroup(state),
+    online: getOnline(state),
   };
 }
 
--- a/client/src/components/Login.js	Thu Aug 03 09:44:37 2017 +0200
+++ b/client/src/components/Login.js	Thu Aug 03 17:33:00 2017 +0200
@@ -8,9 +8,24 @@
 
 class Login extends Component {
 
+  state = {
+    username: '',
+    password: ''
+  }
+
+  handleInputChange = (e) => {
+    const target = e.target;
+    const value = target.value;
+    const name = target.name;
+
+    this.setState({
+      [name]: value
+    });
+  }
+
+
   login = () => {
-    const username = this.username.value;
-    const password = this.password.value;
+    const { username, password } = this.state;
 
     this.props.authActions.loginSubmit(username, password);
   }
@@ -65,12 +80,12 @@
                 <form onSubmit={this.submit}>
                   <FormGroup validationState={ errorMessages && errorMessages.has('username') ? 'error' : null }>
                     <ControlLabel>Username</ControlLabel>
-                    <FormControl componentClass="input" type="text" inputRef={ref => { this.username = ref; }} />
+                    <FormControl componentClass="input" type="text" onChange={this.handleInputChange} name="username" />
                     { this.renderErrorMessage(errorMessages, 'username') }
                   </FormGroup>
                   <FormGroup validationState={ errorMessages && errorMessages.has('password') ? 'error' : null }>
                     <ControlLabel>Password</ControlLabel>
-                    <FormControl componentClass="input" type="password" inputRef={ref => { this.password = ref; }} />
+                    <FormControl componentClass="input" type="password" onChange={this.handleInputChange} name="password" />
                     { this.renderErrorMessage(errorMessages, 'password') }
                   </FormGroup>
                   { this.renderNonFieldErrors(errorMessages) }
--- a/client/src/components/Navbar.js	Thu Aug 03 09:44:37 2017 +0200
+++ b/client/src/components/Navbar.js	Thu Aug 03 17:33:00 2017 +0200
@@ -7,7 +7,9 @@
 import { Navbar, Nav, NavItem, NavDropdown, MenuItem, Modal, Button } from 'react-bootstrap';
 import * as authActions from '../actions/authActions';
 import { forceSync } from '../actions/networkActions';
-import { ActionEnum } from '../constants';
+import { groupSetCurrent } from '../actions/groupActions';
+import { isAuthenticated, getCurrentUser, getOnline, getCurrentGroup, getGroups } from '../selectors/authSelectors';
+import { isSynchronizing, isSynchronized } from '../selectors/syncSelectors';
 import './Navbar.css';
 
 const LoginNav = ({isAuthenticated, currentUser, history, authActions, onLogout}) => {
@@ -63,6 +65,27 @@
   )
 }
 
+const GroupStatus = ({currentGroup, groups, onSelect}) => {
+
+  if(currentGroup) {
+    const currentGroupName = currentGroup.get('name');
+    return (
+      <NavDropdown title={currentGroupName} id="group-dropdown" onSelect={onSelect}>
+        { groups && groups.map((group, key) => {
+            const groupName = group.get('name');
+            const className = (groupName === currentGroupName)?'active':null;
+            return <MenuItem className={className} key={key} eventKey={groupName}>{ groupName }</MenuItem>
+          }
+        )}
+        <MenuItem key="-1" eventKey="__create_group__">Créer un groupe...</MenuItem>
+      </NavDropdown>
+    )
+  } else {
+    return null;
+  }
+
+}
+
 class AppNavbar extends Component {
 
   state = {
@@ -116,6 +139,14 @@
     this.props.networkActions.forceSync();
   }
 
+  onGroupSelect = (groupName) => {
+    if(groupName === "__create_group__") {
+      this.props.history.push('/create-group');
+    } else {
+      this.props.groupActions.groupSetCurrent(groupName);
+    }
+  }
+
   render() {
     return (
       <Navbar fluid inverse fixedTop>
@@ -130,6 +161,7 @@
             <NavItem onClick={this.onClickSessions} href="/sessions">Sessions</NavItem>
           </Nav>
           <Nav pullRight>
+            <GroupStatus currentGroup={this.props.currentGroup} groups={this.props.groups} onSelect={this.onGroupSelect}/>
             <SyncButton id='sync-button' onSyncClick={this.onSyncClick} isSynchronizing={this.props.isSynchronizing} isSynchronized={this.props.isSynchronized} />
             <Online {...this.props} />
             <LoginNav {...this.props} onLogout={this.onClickLogout} />
@@ -159,19 +191,21 @@
 
 function mapStateToProps(state, props) {
   return {
-    isAuthenticated: state.getIn(['authStatus', 'isAuthenticated']),
-    currentUser: state.getIn(['authStatus', 'currentUser']),
-    online: state.getIn(['status', 'online']),
-    isSynchronizing: state.getIn(['status', 'isSynchronizing']),
-    isSynchronized: state.get('notes').every((n) => n.get('action')===ActionEnum.NONE) &&
-      state.get('sessions').every((n) => n.get('action')===ActionEnum.NONE)
+    isAuthenticated: isAuthenticated(state),
+    currentUser: getCurrentUser(state),
+    online: getOnline(state),
+    isSynchronizing: isSynchronizing(state),
+    isSynchronized: isSynchronized(state),
+    currentGroup: getCurrentGroup(state),
+    groups: getGroups(state)
   };
 }
 
 function mapDispatchToProps(dispatch) {
   return {
     authActions: bindActionCreators(authActions, dispatch),
-    networkActions: bindActionCreators({ forceSync }, dispatch)
+    networkActions: bindActionCreators({ forceSync }, dispatch),
+    groupActions: bindActionCreators({ groupSetCurrent }, dispatch)
   }
 }
 
--- a/client/src/components/SessionForm.js	Thu Aug 03 09:44:37 2017 +0200
+++ b/client/src/components/SessionForm.js	Thu Aug 03 17:33:00 2017 +0200
@@ -81,7 +81,7 @@
               ) }
             </FormControl>
           </FormGroup>
-          <FormGroup>
+           <FormGroup>
             <GroupForm session={this.props.session} />
           </FormGroup>
         </form>
--- a/client/src/constants/actionTypes.js	Thu Aug 03 09:44:37 2017 +0200
+++ b/client/src/constants/actionTypes.js	Thu Aug 03 17:33:00 2017 +0200
@@ -40,6 +40,7 @@
 export const GROUP_CREATE_AND_UPDATE_ASYNC = 'GROUP_CREATE_AND_UPDATE_ASYNC';
 export const GROUP_CREATE_SUCCESS = 'GROUP_CREATE_SUCCESS';
 export const GROUP_CREATE_ERROR = 'GROUP_CREATE_ERROR';
+export const GROUP_SET_GROUP = 'GROUP_SET_GROUP';
 
 export const GROUP_LOAD_ASYNC = 'GROUP_LOAD_ASYNC';
 export const GROUP_LOAD_SUCCESS = 'GROUP_LOAD_SUCCESS';
--- a/client/src/index.js	Thu Aug 03 09:44:37 2017 +0200
+++ b/client/src/index.js	Thu Aug 03 17:33:00 2017 +0200
@@ -9,6 +9,7 @@
 import SessionList from './components/SessionList';
 import Session from './components/Session';
 import Login from './components/Login';
+import CreateGroup from './components/CreateGroup';
 import Register from './components/Register';
 import Settings from './components/Settings';
 import './index.css';
@@ -30,6 +31,7 @@
         <Route exact path="/sessions" component={SessionList} />
         <Route exact path="/register" component={Register} />
         <Route exact path="/login" component={Login} />
+        <Route exact path="/create-group" component={CreateGroup} />
         <AuthenticatedRoute exact path="/settings" component={Settings} store={store} />
         <Route exact path="/" component={App} />
       </Switch>
--- a/client/src/reducers/authReducer.js	Thu Aug 03 09:44:37 2017 +0200
+++ b/client/src/reducers/authReducer.js	Thu Aug 03 17:33:00 2017 +0200
@@ -35,6 +35,27 @@
   }
 }
 
+export const currentGroup = (state = null, action) => {
+  switch (action.type) {
+    case types.AUTH_DEAUTHENTICATE:
+    case types.AUTH_LOGOUT:
+      return null;
+    case types.AUTH_LOGIN_SUCCESS:
+      if( state === null) {
+        return action.user.default_group;
+      }
+      return state;
+    case types.GROUP_CREATE_SUCCESS: {
+      return action.group.name;
+    }
+    case types.GROUP_SET_GROUP:
+      return action.group;
+    default:
+      return state;
+  }
+}
+
+
 export const token = (state = null, action) => {
   switch (action.type) {
     case types.AUTH_DEAUTHENTICATE:
--- a/client/src/reducers/index.js	Thu Aug 03 09:44:37 2017 +0200
+++ b/client/src/reducers/index.js	Thu Aug 03 17:33:00 2017 +0200
@@ -3,7 +3,7 @@
 
 import notes from './notesReducer';
 import { sessions } from './sessionsReducer';
-import { isAuthenticated, currentUser, login, register, token, groups, createGroup, clientId } from './authReducer';
+import { isAuthenticated, currentUser, currentGroup, login, register, token, groups, createGroup, clientId } from './authReducer';
 import { autoSubmit, online } from './miscReducer';
 import { isSynchronizing, lastSync } from './syncReducer';
 
@@ -16,6 +16,7 @@
   authStatus: combineReducers({
     token,
     currentUser,
+    currentGroup,
     isAuthenticated,
     clientId,
     lastSync
--- a/client/src/sagas/NoteSyncronizer.js	Thu Aug 03 09:44:37 2017 +0200
+++ b/client/src/sagas/NoteSyncronizer.js	Thu Aug 03 17:33:00 2017 +0200
@@ -1,5 +1,5 @@
 import { select } from 'redux-saga/effects'
-import { getCreatedNotes, getUpdatedNotes, getDeletedNotes } from './selectors';
+import { getCreatedNotes, getUpdatedNotes, getDeletedNotes } from '../selectors/coreSelectors';
 import NoteRecord from '../store/noteRecord';
 import { doDeleteNote, loadNote, resetActionNote } from '../actions/notesActions';
 import Immutable from 'immutable';
--- a/client/src/sagas/SessionSyncronizer.js	Thu Aug 03 09:44:37 2017 +0200
+++ b/client/src/sagas/SessionSyncronizer.js	Thu Aug 03 17:33:00 2017 +0200
@@ -1,5 +1,5 @@
 import { select } from 'redux-saga/effects'
-import { getCreatedSessions, getUpdatedSessions, getDeletedSessions } from './selectors';
+import { getCreatedSessions, getUpdatedSessions, getDeletedSessions } from '../selectors/coreSelectors';
 import { ActionEnum } from '../constants';
 import moment from 'moment';
 import SessionRecord from '../store/sessionRecord';
--- a/client/src/sagas/authSaga.js	Thu Aug 03 09:44:37 2017 +0200
+++ b/client/src/sagas/authSaga.js	Thu Aug 03 17:33:00 2017 +0200
@@ -95,7 +95,7 @@
       });
       yield put({ type: types.USER_UPDATE_SETTINGS, firstname, lastname });
     } catch (e) {
-
+      //TODO: handle error
     }
   }
 }
--- a/client/src/sagas/groupSaga.js	Thu Aug 03 09:44:37 2017 +0200
+++ b/client/src/sagas/groupSaga.js	Thu Aug 03 17:33:00 2017 +0200
@@ -10,6 +10,7 @@
     try {
       const response = yield client.post('/api/auth/group/', group);
       yield put(groupCreateSuccess(response));
+      context.history.push('/sessions');
     } catch (e) {
       yield put(groupCreateError(e));
     }
--- a/client/src/sagas/networkSaga.js	Thu Aug 03 09:44:37 2017 +0200
+++ b/client/src/sagas/networkSaga.js	Thu Aug 03 17:33:00 2017 +0200
@@ -6,7 +6,7 @@
 import moment from 'moment';
 import { delay } from 'redux-saga';
 import { setOnlineStatus } from '../actions/networkActions';
-import { getToken } from './selectors';
+import { getToken } from '../selectors/authSelectors';
 
 
 
--- a/client/src/sagas/selectors.js	Thu Aug 03 09:44:37 2017 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-// Define state selector for saga
-import Immutable from 'immutable';
-import { ActionEnum } from '../constants'
-
-export const getLastSync = state => state.getIn(['authStatus', 'lastSync']) || 0
-
-export const getOnline = state => state.getIn(["status", 'online'])
-
-export const getToken = state => state.getIn(['authStatus','token'])
-
-const getSessionMapSelector = actionVal => state =>
-  state.get('sessions')
-    .filter(s => s.get('action') === actionVal)
-    .reduce(
-      (res, obj) => {
-        return res.set(obj.get('_id'), obj);
-      },
-      Immutable.Map()
-    );
-
-const getNoteMapSelector = actionVal => state => {
-  const deletedSessions = state.get('sessions')
-    .filter(s => s.get('action') === ActionEnum.DELETED)
-    .reduce(
-      (res, obj) => {
-        return res.set(obj.get('_id'), obj);
-      },
-      Immutable.Map()
-    );
-  return state.get('notes')
-    .filter(n => (n.get('action') === actionVal && !deletedSessions.has(n.get('session'))))
-    .reduce(
-      (res, obj) => {
-        return res.set(obj.get('_id'), obj);
-      },
-      Immutable.Map()
-    );
-}
-
-
-export const getUpdatedSessions = getSessionMapSelector(ActionEnum.UPDATED);
-
-export const getCreatedSessions = getSessionMapSelector(ActionEnum.CREATED);
-
-export const getDeletedSessions = getSessionMapSelector(ActionEnum.DELETED);
-
-export const getUpdatedNotes = getNoteMapSelector(ActionEnum.UPDATED);
-
-export const getCreatedNotes = getNoteMapSelector(ActionEnum.CREATED);
-
-export const getDeletedNotes = getNoteMapSelector(ActionEnum.DELETED);
--- a/client/src/sagas/syncSaga.js	Thu Aug 03 09:44:37 2017 +0200
+++ b/client/src/sagas/syncSaga.js	Thu Aug 03 17:33:00 2017 +0200
@@ -1,7 +1,8 @@
 import { put, take, all, select, spawn, race, call, fork} from 'redux-saga/effects'
 import { delay } from 'redux-saga';
 import * as types from '../constants/actionTypes';
-import { getLastSync, getOnline } from './selectors';
+import { getLastSync } from '../selectors/syncSelectors';
+import { getOnline } from '../selectors/authSelectors';
 import moment from 'moment';
 import { startSynchronize, endSynchronize, updateLastSync } from '../actions/syncActions';
 import { forceSync } from '../actions/networkActions';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/selectors/authSelectors.js	Thu Aug 03 17:33:00 2017 +0200
@@ -0,0 +1,30 @@
+// Selectors linked to the authentication status
+
+export const getOnline = state => state.getIn(["status", 'online'])
+
+export const getToken = state => state.getIn(['authStatus','token'])
+
+export const isAuthenticated = state => state.getIn(['authStatus', 'isAuthenticated'])
+
+export const getCurrentUser = state => state.getIn(['authStatus', 'currentUser'])
+
+export const getClientId = state => state.getIn(['authStatus', 'clientId'])
+
+export const getCurrentGroupName = state => state.getIn(['authStatus', 'currentGroup'])
+
+export const getGroups = state => state.get('groups')
+
+export const getCurrentGroup = state => {
+  const groupName = getCurrentGroupName(state);
+  const groups = getGroups(state);
+  if(groups) {
+    return groups.find( g => g.get('name') === groupName );
+  } else {
+    return null;
+  }
+}
+
+export const getCreateGroup = state => state.get('createGroup')
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/selectors/coreSelectors.js	Thu Aug 03 17:33:00 2017 +0200
@@ -0,0 +1,45 @@
+import Immutable from 'immutable';
+import { ActionEnum } from '../constants'
+
+
+const getSessionMapSelector = actionVal => state =>
+  state.get('sessions')
+    .filter(s => s.get('action') === actionVal)
+    .reduce(
+      (res, obj) => {
+        return res.set(obj.get('_id'), obj);
+      },
+      Immutable.Map()
+    );
+
+const getNoteMapSelector = actionVal => state => {
+  const deletedSessions = state.get('sessions')
+    .filter(s => s.get('action') === ActionEnum.DELETED)
+    .reduce(
+      (res, obj) => {
+        return res.set(obj.get('_id'), obj);
+      },
+      Immutable.Map()
+    );
+  return state.get('notes')
+    .filter(n => (n.get('action') === actionVal && !deletedSessions.has(n.get('session'))))
+    .reduce(
+      (res, obj) => {
+        return res.set(obj.get('_id'), obj);
+      },
+      Immutable.Map()
+    );
+}
+
+
+export const getUpdatedSessions = getSessionMapSelector(ActionEnum.UPDATED);
+
+export const getCreatedSessions = getSessionMapSelector(ActionEnum.CREATED);
+
+export const getDeletedSessions = getSessionMapSelector(ActionEnum.DELETED);
+
+export const getUpdatedNotes = getNoteMapSelector(ActionEnum.UPDATED);
+
+export const getCreatedNotes = getNoteMapSelector(ActionEnum.CREATED);
+
+export const getDeletedNotes = getNoteMapSelector(ActionEnum.DELETED);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/selectors/syncSelectors.js	Thu Aug 03 17:33:00 2017 +0200
@@ -0,0 +1,10 @@
+// selectors linked to syncronization status
+import { ActionEnum } from '../constants';
+
+export const getLastSync = state => state.getIn(['authStatus', 'lastSync']) || 0
+
+export const isSynchronizing = state => state.getIn(['status', 'isSynchronizing'])
+
+export const isSynchronized = state =>
+    state.get('notes').every((n) => n.get('action')===ActionEnum.NONE) &&
+    state.get('sessions').every((n) => n.get('action')===ActionEnum.NONE)
--- a/client/src/store/configureStore.js	Thu Aug 03 09:44:37 2017 +0200
+++ b/client/src/store/configureStore.js	Thu Aug 03 17:33:00 2017 +0200
@@ -38,6 +38,7 @@
     clientId: null,
     lastSync: 0,
     currentUser: null,
+    currentGroup: null
   }),
   autoSubmit: false,
   login: asyncRequest,