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