# HG changeset patch # User Alexandre Segura # Date 1497283958 -7200 # Node ID 4cfeabef7d5e9744f07ed2321a1d87e536319908 # Parent abf9f3ff2635b6105d0c90eecef8ebc83cf5a883 Store data in PouchDB. - Introduce redux-saga to perform async actions. - Refactor actions to be async. diff -r abf9f3ff2635 -r 4cfeabef7d5e client/package.json --- 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" }, diff -r abf9f3ff2635 -r 4cfeabef7d5e client/src/actions/notesActions.js --- 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 + } +} diff -r abf9f3ff2635 -r 4cfeabef7d5e client/src/actions/sessionsActions.js --- 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 + } +} diff -r abf9f3ff2635 -r 4cfeabef7d5e client/src/components/NoteInput.js --- 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); diff -r abf9f3ff2635 -r 4cfeabef7d5e client/src/components/NotesList.js --- 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 (
{notes.map((note) => - + )}
); diff -r abf9f3ff2635 -r 4cfeabef7d5e client/src/components/Session.js --- 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 (
@@ -34,7 +38,7 @@ Title { this.title = ref; }} /> @@ -43,7 +47,7 @@ Description { 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 }; } diff -r abf9f3ff2635 -r 4cfeabef7d5e client/src/components/Sessions.js --- 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 @@ {this.props.sessions.map((session) => 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')} )} - + @@ -49,7 +53,7 @@ function mapDispatchToProps(dispatch) { return { - actions: bindActionCreators(sessionsActions, dispatch) + sessionsActions: bindActionCreators(sessionsActions, dispatch) } } diff -r abf9f3ff2635 -r 4cfeabef7d5e client/src/constants/actionTypes.js --- 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'; diff -r abf9f3ff2635 -r 4cfeabef7d5e client/src/reducers/notesReducer.js --- 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; } diff -r abf9f3ff2635 -r 4cfeabef7d5e client/src/reducers/sessionsReducer.js --- 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; } diff -r abf9f3ff2635 -r 4cfeabef7d5e client/src/sagas/index.js --- /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(), + ]) +} diff -r abf9f3ff2635 -r 4cfeabef7d5e client/src/store/configureStore.js --- 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; }; diff -r abf9f3ff2635 -r 4cfeabef7d5e client/src/store/noteRecord.js --- 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: '', diff -r abf9f3ff2635 -r 4cfeabef7d5e client/yarn.lock --- 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"