Store data in PouchDB.
- Introduce redux-saga to perform async actions.
- Refactor actions to be async.
--- a/client/package.json Mon Jun 12 18:09:13 2017 +0200
+++ b/client/package.json Mon Jun 12 18:12:38 2017 +0200
@@ -9,6 +9,7 @@
"lodash": "^4.17.4",
"moment": "^2.18.1",
"pouchdb": "^6.2.0",
+ "pouchdb-find": "^6.2.0",
"react": "^15.5.4",
"react-bootstrap": "^0.31.0",
"react-dom": "^15.5.4",
@@ -17,6 +18,7 @@
"react-router-redux": "next",
"redux": "^3.6.0",
"redux-immutable": "^4.0.0",
+ "redux-saga": "^0.15.3",
"slate": "^0.20.1",
"uuid": "^3.0.1"
},
--- a/client/src/actions/notesActions.js Mon Jun 12 18:09:13 2017 +0200
+++ b/client/src/actions/notesActions.js Mon Jun 12 18:12:38 2017 +0200
@@ -4,10 +4,10 @@
export const addNote = (session, data) => {
return {
- type: types.ADD_NOTE,
+ type: types.ADD_NOTE_ASYNC,
note: {
- id: uuidV1(),
- session: session.id,
+ _id: uuidV1(),
+ session: session._id,
raw: data.raw,
plain: data.plain,
html: data.html,
@@ -17,3 +17,10 @@
}
};
}
+
+export const loadNotesBySession = (session) => {
+ return {
+ type: types.LOAD_NOTES_BY_SESSION_ASYNC,
+ session: session
+ }
+}
--- a/client/src/actions/sessionsActions.js Mon Jun 12 18:09:13 2017 +0200
+++ b/client/src/actions/sessionsActions.js Mon Jun 12 18:12:38 2017 +0200
@@ -2,11 +2,11 @@
import * as types from '../constants/actionTypes';
-export const createNewSession = () => {
+export const createSession = () => {
return {
- type: types.NEW_SESSION,
+ type: types.CREATE_SESSION_ASYNC,
session: {
- id: uuidV1(),
+ _id: uuidV1(),
date: new Date(),
title: '',
description: '',
@@ -21,3 +21,9 @@
values: values,
};
}
+
+export const loadSessions = () => {
+ return {
+ type: types.LOAD_SESSIONS_ASYNC
+ }
+}
--- a/client/src/components/NoteInput.js Mon Jun 12 18:09:13 2017 +0200
+++ b/client/src/components/NoteInput.js Mon Jun 12 18:12:38 2017 +0200
@@ -1,10 +1,13 @@
-import React, {Component} from 'react';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
import { Form, FormGroup, Button, Label, Row, Col } from 'react-bootstrap';
import moment from 'moment';
import PropTypes from 'prop-types';
import SlateEditor from './SlateEditor';
import Clock from './Clock'
+import * as notesActions from '../actions/notesActions';
class NoteInput extends Component {
@@ -28,7 +31,7 @@
const html = this.refs.editor.asHtml();
const categories = this.refs.editor.asCategories();
- this.props.addNote(this.props.session, {
+ this.props.notesActions.addNote(this.props.session, {
plain: plain,
raw: raw,
html: html,
@@ -84,8 +87,18 @@
}
}
+function mapStateToProps(state, props) {
+ return {};
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ notesActions: bindActionCreators(notesActions, dispatch),
+ }
+}
+
NoteInput.propTypes = {
addNote: PropTypes.func.isRequired
};
-export default NoteInput;
+export default connect(mapStateToProps, mapDispatchToProps)(NoteInput);
--- a/client/src/components/NotesList.js Mon Jun 12 18:09:13 2017 +0200
+++ b/client/src/components/NotesList.js Mon Jun 12 18:12:38 2017 +0200
@@ -18,7 +18,7 @@
return (
<div>
{notes.map((note) =>
- <Note note={note} key={note.id} />
+ <Note note={note} key={note._id} />
)}
</div>
);
--- a/client/src/components/Session.js Mon Jun 12 18:09:13 2017 +0200
+++ b/client/src/components/Session.js Mon Jun 12 18:12:38 2017 +0200
@@ -21,6 +21,10 @@
});
}
+ componentDidMount = () => {
+ this.props.notesActions.loadNotesBySession({ _id: this.props.match.params.id });
+ }
+
render() {
return (
<div>
@@ -34,7 +38,7 @@
<ControlLabel>Title</ControlLabel>
<FormControl
type="text"
- defaultValue={this.props.currentSession.title}
+ defaultValue={this.props.currentSession ? this.props.currentSession.title : ''}
placeholder="Enter a title for this session"
inputRef={ref => { this.title = ref; }}
/>
@@ -43,7 +47,7 @@
<ControlLabel>Description</ControlLabel>
<FormControl
componentClass="textarea"
- defaultValue={this.props.currentSession.description}
+ defaultValue={this.props.currentSession ? this.props.currentSession.description : ''}
placeholder="Enter a description for this session"
inputRef={ref => { this.description = ref; }}
/>
@@ -70,14 +74,12 @@
const sessions = state.get('sessions');
const notes = state.get('notes');
-
- const currentSession = sessions.find(session => session.id === sessionId);
- const notesBySession = notes.filter(note => note.session === sessionId);
+ const currentSession = sessions.find(session => session._id === sessionId);
return {
currentSession: currentSession,
sessions: sessions,
- notes: notesBySession
+ notes: notes
};
}
--- a/client/src/components/Sessions.js Mon Jun 12 18:09:13 2017 +0200
+++ b/client/src/components/Sessions.js Mon Jun 12 18:12:38 2017 +0200
@@ -9,10 +9,14 @@
class Sessions extends Component {
- createNewSession = () => {
- const result = this.props.actions.createNewSession();
- // FIXME Feels ugly, change location after state has changed?
- this.props.history.push('/sessions/' + result.session.id)
+ createSession = () => {
+ this.props.sessionsActions.createSession();
+ }
+
+ componentDidUpdate = () => {
+ if (this.props.currentSession) {
+ this.props.history.push('/sessions/' + this.props.currentSession._id)
+ }
}
render() {
@@ -25,13 +29,13 @@
<ListGroup>
{this.props.sessions.map((session) =>
<ListGroupItem
- key={session.id}
- onClick={() => this.props.history.push('/sessions/' + session.id)}>
- {session.title || 'No title'} {session.id} {moment(session.date).format('DD/MM/YYYY')}
+ key={session._id}
+ onClick={() => this.props.history.push('/sessions/' + session._id)}>
+ {session.title || 'No title'} {session._id} {moment(session.date).format('DD/MM/YYYY')}
</ListGroupItem>
)}
</ListGroup>
- <Button bsStyle="success" onClick={this.createNewSession}>Create new session</Button>
+ <Button bsStyle="success" onClick={this.createSession}>Create new session</Button>
</Col>
</Row>
</Grid>
@@ -49,7 +53,7 @@
function mapDispatchToProps(dispatch) {
return {
- actions: bindActionCreators(sessionsActions, dispatch)
+ sessionsActions: bindActionCreators(sessionsActions, dispatch)
}
}
--- a/client/src/constants/actionTypes.js Mon Jun 12 18:09:13 2017 +0200
+++ b/client/src/constants/actionTypes.js Mon Jun 12 18:12:38 2017 +0200
@@ -1,3 +1,10 @@
export const ADD_NOTE = 'ADD_NOTE';
-export const NEW_SESSION = 'NEW_SESSION';
+export const ADD_NOTE_ASYNC = 'ADD_NOTE_ASYNC';
+export const LOAD_NOTES_BY_SESSION = 'LOAD_NOTES_BY_SESSION';
+export const LOAD_NOTES_BY_SESSION_ASYNC = 'LOAD_NOTES_BY_SESSION_ASYNC';
+
+export const CREATE_SESSION = 'CREATE_SESSION';
+export const CREATE_SESSION_ASYNC = 'CREATE_SESSION_ASYNC';
export const UPDATE_SESSION = 'UPDATE_SESSION';
+export const LOAD_SESSIONS = 'LOAD_SESSIONS';
+export const LOAD_SESSIONS_ASYNC = 'LOAD_SESSIONS_ASYNC';
--- a/client/src/reducers/notesReducer.js Mon Jun 12 18:09:13 2017 +0200
+++ b/client/src/reducers/notesReducer.js Mon Jun 12 18:12:38 2017 +0200
@@ -1,19 +1,13 @@
import Immutable from 'immutable';
-// import PouchDB from 'pouchdb';
-
import * as types from '../constants/actionTypes';
import noteRecord from '../store/noteRecord';
-// const db = new PouchDB('notes');
-
-// db.allDocs({ include_docs: true, descending: true }, function(err, doc) {
-// console.log(doc.rows)
-// });
-
export default (state = Immutable.List([]), action) => {
switch (action.type) {
case types.ADD_NOTE:
return state.push(new noteRecord(action.note));
+ case types.LOAD_NOTES_BY_SESSION:
+ return action.notes;
default:
return state;
}
--- a/client/src/reducers/sessionsReducer.js Mon Jun 12 18:09:13 2017 +0200
+++ b/client/src/reducers/sessionsReducer.js Mon Jun 12 18:12:38 2017 +0200
@@ -3,7 +3,7 @@
export const currentSession = (state = null, action) => {
switch (action.type) {
- case types.NEW_SESSION:
+ case types.CREATE_SESSION:
return action.session;
default:
return state;
@@ -12,7 +12,7 @@
export const sessions = (state = Immutable.List([]), action) => {
switch (action.type) {
- case types.NEW_SESSION:
+ case types.CREATE_SESSION:
return state.push(action.session);
case types.UPDATE_SESSION:
const sessionToUpdate = state.find(session => session === action.session);
@@ -22,6 +22,8 @@
}
const updatedSession = Object.assign({}, sessionToUpdate, action.values);
return state.set(sessionIndex, updatedSession);
+ case types.LOAD_SESSIONS:
+ return action.sessions;
default:
return state;
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/sagas/index.js Mon Jun 12 18:12:38 2017 +0200
@@ -0,0 +1,77 @@
+import PouchDB from 'pouchdb'
+import { put, takeLatest, all } from 'redux-saga/effects'
+import * as types from '../constants/actionTypes';
+import PouchDBFind from 'pouchdb-find';
+import Immutable from 'immutable';
+
+PouchDB.debug.disable();
+PouchDB.plugin(PouchDBFind);
+
+const sessionsDB = new PouchDB('sessions');
+const notesDB = new PouchDB('notes');
+notesDB.createIndex({
+ index: { fields: ['session'] }
+});
+
+// ---
+
+export function* loadSessions() {
+ const response = yield sessionsDB.allDocs({ include_docs: true })
+ const sessions = response.rows.map(row => row.doc)
+ yield put({ type: types.LOAD_SESSIONS, sessions: Immutable.List(sessions) })
+}
+
+export function* watchLoadSessions() {
+ yield takeLatest(types.LOAD_SESSIONS_ASYNC, loadSessions)
+}
+
+// ---
+
+export function* createSession(action) {
+ const response = yield sessionsDB.put(action.session);
+ // TODO Error control
+ const session = Object.assign({}, action.session, { rev: response.rev });
+ yield put({ type: types.CREATE_SESSION, session: session })
+}
+
+export function* watchCreateSession() {
+ yield takeLatest(types.CREATE_SESSION_ASYNC, createSession)
+}
+
+// ---
+
+export function* addNote(action) {
+ const response = yield notesDB.put(action.note);
+ // TODO Error control
+ const note = Object.assign({}, action.note, { rev: response.rev });
+ yield put({ type: types.ADD_NOTE, note: note })
+}
+
+export function* watchAddNote() {
+ yield takeLatest(types.ADD_NOTE_ASYNC, addNote)
+}
+
+// ---
+
+export function* loadNotesBySession(action) {
+ const result = yield notesDB.find({
+ selector: { session: action.session._id },
+ // sort: ['name']
+ });
+ yield put({ type: types.LOAD_NOTES_BY_SESSION, notes: Immutable.List(result.docs) })
+}
+
+export function* watchLoadNotesBySession() {
+ yield takeLatest(types.LOAD_NOTES_BY_SESSION_ASYNC, loadNotesBySession)
+}
+
+// ---
+
+export default function* rootSaga() {
+ yield all([
+ watchLoadSessions(),
+ watchLoadNotesBySession(),
+ watchAddNote(),
+ watchCreateSession(),
+ ])
+}
--- a/client/src/store/configureStore.js Mon Jun 12 18:09:13 2017 +0200
+++ b/client/src/store/configureStore.js Mon Jun 12 18:12:38 2017 +0200
@@ -1,32 +1,15 @@
import rootReducer from '../reducers';
+import rootSaga from '../sagas';
+import { loadSessions } from '../actions/sessionsActions';
import { createStore, applyMiddleware } from 'redux';
import { routerMiddleware } from 'react-router-redux';
+import createSagaMiddleware from 'redux-saga'
import Immutable from 'immutable';
-const loadState = () => {
- try {
- const serializedState = localStorage.getItem('state');
- if (!serializedState) {
- return {};
- }
- return JSON.parse(serializedState);
- } catch (err) {
- return {};
- }
-}
-
-const saveState = (state) => {
- try {
- localStorage.setItem('state', JSON.stringify(state));
- } catch (err) {}
-}
-
-const savedState = loadState();
-
const defaultState = {
currentSession: null,
- sessions: Immutable.List(savedState.sessions || []),
- notes: Immutable.List(savedState.notes || []),
+ sessions: Immutable.List([]),
+ notes: Immutable.List([]),
isAuthenticated: false,
};
@@ -34,18 +17,14 @@
export default (history, initialState = storeInitialState) => {
- const middleware = routerMiddleware(history);
- const store = createStore(rootReducer, initialState, applyMiddleware(middleware));
+ const router = routerMiddleware(history);
+ const saga = createSagaMiddleware();
- store.subscribe(() => {
- const state = store.getState().toJSON();
- const stateToPersist = {
- sessions: state.sessions,
- notes: state.notes,
- };
+ const store = createStore(rootReducer, initialState, applyMiddleware(router, saga));
- saveState(stateToPersist);
- });
+ saga.run(rootSaga)
+
+ store.dispatch(loadSessions());
return store;
};
--- a/client/src/store/noteRecord.js Mon Jun 12 18:09:13 2017 +0200
+++ b/client/src/store/noteRecord.js Mon Jun 12 18:12:38 2017 +0200
@@ -1,7 +1,7 @@
import Immutable from 'immutable';
export default Immutable.Record({
- id: '',
+ _id: '',
session: '',
plain: '',
--- a/client/yarn.lock Mon Jun 12 18:09:13 2017 +0200
+++ b/client/yarn.lock Mon Jun 12 18:12:38 2017 +0200
@@ -5305,6 +5305,90 @@
source-map "^0.5.6"
supports-color "^3.2.3"
+pouchdb-abstract-mapreduce@6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/pouchdb-abstract-mapreduce/-/pouchdb-abstract-mapreduce-6.2.0.tgz#55858d1799c89290185df56b71c843a481abcbae"
+ dependencies:
+ pouchdb-binary-utils "6.2.0"
+ pouchdb-collate "6.2.0"
+ pouchdb-collections "6.2.0"
+ pouchdb-mapreduce-utils "6.2.0"
+ pouchdb-md5 "6.2.0"
+ pouchdb-promise "6.2.0"
+ pouchdb-utils "6.2.0"
+
+pouchdb-binary-utils@6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/pouchdb-binary-utils/-/pouchdb-binary-utils-6.2.0.tgz#dc4154c01b92fb9ad87fdf695394a91b5d9429bf"
+ dependencies:
+ buffer-from "0.1.1"
+
+pouchdb-collate@6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/pouchdb-collate/-/pouchdb-collate-6.2.0.tgz#9ee5e578de004581c148754f7decdc0b70495ffc"
+
+pouchdb-collections@6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/pouchdb-collections/-/pouchdb-collections-6.2.0.tgz#f532601870cbd329ba0c6005bcdd301126825be2"
+
+pouchdb-errors@6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/pouchdb-errors/-/pouchdb-errors-6.2.0.tgz#5ccbefaf2b92e918d5d90b5a5b779376464b5907"
+ dependencies:
+ inherits "2.0.3"
+
+pouchdb-find@^6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/pouchdb-find/-/pouchdb-find-6.2.0.tgz#a824e158e25b0816614d67e680a4d8c8a694fa1e"
+ dependencies:
+ pouchdb-abstract-mapreduce "6.2.0"
+ pouchdb-collate "6.2.0"
+ pouchdb-md5 "6.2.0"
+ pouchdb-promise "6.2.0"
+ pouchdb-selector-core "6.2.0"
+ pouchdb-utils "6.2.0"
+
+pouchdb-mapreduce-utils@6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/pouchdb-mapreduce-utils/-/pouchdb-mapreduce-utils-6.2.0.tgz#fe6c15eff1a6fe48d68f973c7a79ef492652ca2f"
+ dependencies:
+ argsarray "0.0.1"
+ inherits "2.0.3"
+ pouchdb-collections "6.2.0"
+ pouchdb-utils "6.2.0"
+
+pouchdb-md5@6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/pouchdb-md5/-/pouchdb-md5-6.2.0.tgz#7581fc4e932c57c78ee48d25a7d0014bff008e4d"
+ dependencies:
+ pouchdb-binary-utils "6.2.0"
+ spark-md5 "3.0.0"
+
+pouchdb-promise@6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/pouchdb-promise/-/pouchdb-promise-6.2.0.tgz#1e4fcec0aa0678df583e6a7b4b996ba010a608fe"
+ dependencies:
+ lie "3.1.1"
+
+pouchdb-selector-core@6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/pouchdb-selector-core/-/pouchdb-selector-core-6.2.0.tgz#ab753c17182bc394e6a6b1de275331161f85e325"
+ dependencies:
+ pouchdb-collate "6.2.0"
+ pouchdb-utils "6.2.0"
+
+pouchdb-utils@6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/pouchdb-utils/-/pouchdb-utils-6.2.0.tgz#cd8d4207a34e478b49af201ff0c51ea733244061"
+ dependencies:
+ argsarray "0.0.1"
+ clone-buffer "1.0.0"
+ immediate "3.0.6"
+ inherits "2.0.3"
+ pouchdb-collections "6.2.0"
+ pouchdb-errors "6.2.0"
+ pouchdb-promise "6.2.0"
+
pouchdb@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/pouchdb/-/pouchdb-6.2.0.tgz#5c8521b46cfc83644ca7fc61a7b240d2ce17dc0d"
@@ -5820,6 +5904,10 @@
version "4.0.0"
resolved "https://registry.yarnpkg.com/redux-immutable/-/redux-immutable-4.0.0.tgz#3a1a32df66366462b63691f0e1dc35e472bbc9f3"
+redux-saga@^0.15.3:
+ version "0.15.3"
+ resolved "https://registry.yarnpkg.com/redux-saga/-/redux-saga-0.15.3.tgz#be2b86b4ad46bf0d84fcfcb0ca96cfc33db91acb"
+
redux@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/redux/-/redux-3.6.0.tgz#887c2b3d0b9bd86eca2be70571c27654c19e188d"