Store data in PouchDB.
authorAlexandre Segura <mex.zktk@gmail.com>
Mon, 12 Jun 2017 18:12:38 +0200
changeset 29 4cfeabef7d5e
parent 28 abf9f3ff2635
child 30 4d93f4ed95bc
Store data in PouchDB. - Introduce redux-saga to perform async actions. - Refactor actions to be async.
client/package.json
client/src/actions/notesActions.js
client/src/actions/sessionsActions.js
client/src/components/NoteInput.js
client/src/components/NotesList.js
client/src/components/Session.js
client/src/components/Sessions.js
client/src/constants/actionTypes.js
client/src/reducers/notesReducer.js
client/src/reducers/sessionsReducer.js
client/src/sagas/index.js
client/src/store/configureStore.js
client/src/store/noteRecord.js
client/yarn.lock
--- 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"