diff --git a/.eslintignore b/.eslintignore
index 0800076..fb02106 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -10,3 +10,6 @@ build
# Ignore Logs
logs
*.log
+
+# coverage test directory
+coverage
diff --git a/.eslintrc b/.eslintrc
index 2225fd6..762feb8 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,3 +1,11 @@
{
- "extends": ["standard", "standard-react"]
+ "extends": ["standard", "standard-react"],
+ "settings": {
+ "react": {
+ "version": "16.4.0"
+ }
+ },
+ "env": {
+ "jest": true
+ }
}
diff --git a/.gitignore b/.gitignore
index a955a19..18817a9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,6 @@ build
# distribution directory
dist
+
+# coverage test directory
+coverage
diff --git a/.travis.yml b/.travis.yml
index d418413..6ee8dab 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,3 +12,4 @@ install:
script:
- npm run lint
- npm run build
+ - npm run test
diff --git a/AUTHORS.md b/AUTHORS.md
index 0ce9c6a..16075b4 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -14,6 +14,7 @@ If you make a contribution here, you may add your name and email address here.
- Austin Daniel (austindanielfrench@gmail.com)
- Bekir Durak (bekirdurak97@gmail.com)
- Andrew Cheng (cheng0807@gmail.com)
+- Mahmoud Younes (m.younesbadr@gmail.com)
diff --git a/README.md b/README.md
index 9ccb465..da29c22 100644
--- a/README.md
+++ b/README.md
@@ -30,10 +30,10 @@
### Getting started
-Check [Our Docs](./docs/index.md) for **building** and **installing** instructions.
+Check [Our Docs](./docs/README.md) for **building** and **installing** instructions.
-If you would like to contribute to Deer, see [Our Docs](./docs/index.md) too.
+If you would like to contribute to Deer, see [Our Docs](./docs/README.md) too.
### Get in touch!
diff --git a/__mocks__/electron.js b/__mocks__/electron.js
new file mode 100644
index 0000000..e1c2053
--- /dev/null
+++ b/__mocks__/electron.js
@@ -0,0 +1,5 @@
+module.exports = {
+ remote: {
+ getGlobal: jest.fn()
+ }
+}
diff --git a/__mocks__/pouchdb-browser.js b/__mocks__/pouchdb-browser.js
new file mode 100644
index 0000000..a3edaa5
--- /dev/null
+++ b/__mocks__/pouchdb-browser.js
@@ -0,0 +1,3 @@
+module.exports = jest.fn(() => ({
+ name: 'test'
+}))
diff --git a/__mocks__/uuid/v4.js b/__mocks__/uuid/v4.js
new file mode 100644
index 0000000..6200f47
--- /dev/null
+++ b/__mocks__/uuid/v4.js
@@ -0,0 +1 @@
+module.exports = jest.fn(() => 'id-string')
diff --git a/__tests__/actions/__snapshots__/header.test.js.snap b/__tests__/actions/__snapshots__/header.test.js.snap
new file mode 100644
index 0000000..ef71de6
--- /dev/null
+++ b/__tests__/actions/__snapshots__/header.test.js.snap
@@ -0,0 +1,28 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`header_actions setDeleteDisabled Dispatches the correct action and payload 1`] = `
+Array [
+ Object {
+ "payload": true,
+ "type": "UPDATE_DELETE_NOTE_STATE",
+ },
+]
+`;
+
+exports[`header_actions setNewNoteDisabled Dispatches the correct action and payload 1`] = `
+Array [
+ Object {
+ "payload": true,
+ "type": "UPDATE_NEW_NOTE_STATE",
+ },
+]
+`;
+
+exports[`header_actions setSaveDisabled Dispatches the correct action and payload 1`] = `
+Array [
+ Object {
+ "payload": true,
+ "type": "UPDATE_SAVE_NOTE_STATE",
+ },
+]
+`;
diff --git a/__tests__/actions/__snapshots__/modal.test.js.snap b/__tests__/actions/__snapshots__/modal.test.js.snap
new file mode 100644
index 0000000..407b785
--- /dev/null
+++ b/__tests__/actions/__snapshots__/modal.test.js.snap
@@ -0,0 +1,10 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`modal_actions toggleYesNoModal Dispatches the correct action and payload 1`] = `
+Array [
+ Object {
+ "payload": "SAVE_NOTE",
+ "type": "TOGGLE_SAVE_MODAL",
+ },
+]
+`;
diff --git a/__tests__/actions/__snapshots__/note.test.js.snap b/__tests__/actions/__snapshots__/note.test.js.snap
new file mode 100644
index 0000000..b0c52de
--- /dev/null
+++ b/__tests__/actions/__snapshots__/note.test.js.snap
@@ -0,0 +1,80 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`note_actions addNewNote Dispatches the correct action and payload 1`] = `
+Array [
+ Object {
+ "type": "ADD_NOTE",
+ },
+]
+`;
+
+exports[`note_actions deleteNoteFromList Dispatches the correct action and payload 1`] = `
+Array [
+ Object {
+ "payload": 3,
+ "type": "DELETE_NOTE_FROM_LIST",
+ },
+]
+`;
+
+exports[`note_actions loadNoteContent Dispatches the correct action and payload 1`] = `
+Array [
+ Object {
+ "payload": Object {},
+ "type": "LOAD_NOTE_CONTENT",
+ },
+]
+`;
+
+exports[`note_actions setActiveNoteIndex Dispatches the correct action and payload 1`] = `
+Array [
+ Object {
+ "payload": 3,
+ "type": "SET_ACTIVE_NOTE",
+ },
+]
+`;
+
+exports[`note_actions setNoteStatus Dispatches the correct action and payload 1`] = `
+Array [
+ Object {
+ "payload": "SAVING_NOTE",
+ "type": "SET_NOTE_STATUS",
+ },
+]
+`;
+
+exports[`note_actions updateActiveNoteState Dispatches the correct action and payload 1`] = `
+Array [
+ Object {
+ "payload": Object {},
+ "type": "UPDATE_ACTIVE_NOTE_STATE",
+ },
+]
+`;
+
+exports[`note_actions updateNoteList Dispatches the correct action and payload 1`] = `
+Array [
+ Object {
+ "type": "UPDATE_NOTE_LIST",
+ },
+]
+`;
+
+exports[`note_actions updateNoteRev Dispatches the correct action and payload 1`] = `
+Array [
+ Object {
+ "payload": "a123-a123",
+ "type": "UPDATE_NOTE_REV",
+ },
+]
+`;
+
+exports[`note_actions updateNoteTitle Dispatches the correct action and payload 1`] = `
+Array [
+ Object {
+ "payload": "Hello world",
+ "type": "UPDATE_NOTE_TITLE",
+ },
+]
+`;
diff --git a/__tests__/actions/__snapshots__/welcome.test.js.snap b/__tests__/actions/__snapshots__/welcome.test.js.snap
new file mode 100644
index 0000000..92e7902
--- /dev/null
+++ b/__tests__/actions/__snapshots__/welcome.test.js.snap
@@ -0,0 +1,17 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`welcome_actions getNextLang Dispatches the correct action and payload 1`] = `
+Array [
+ Object {
+ "type": "SET_NEXT_LANG",
+ },
+]
+`;
+
+exports[`welcome_actions toggleFade Dispatches the correct action and payload 1`] = `
+Array [
+ Object {
+ "type": "TOGGLE_FADE",
+ },
+]
+`;
diff --git a/__tests__/actions/header.test.js b/__tests__/actions/header.test.js
new file mode 100644
index 0000000..f87b598
--- /dev/null
+++ b/__tests__/actions/header.test.js
@@ -0,0 +1,34 @@
+import configureStore from 'redux-mock-store'
+
+// Actions to be tested
+import * as headerActions from '../../app/actions/header'
+
+const mockStore = configureStore()
+const store = mockStore()
+
+describe('header_actions', () => {
+ beforeEach(() => {
+ store.clearActions()
+ })
+
+ describe('setNewNoteDisabled', () => {
+ test('Dispatches the correct action and payload', () => {
+ store.dispatch(headerActions.setNewNoteDisabled(true))
+ expect(store.getActions()).toMatchSnapshot()
+ })
+ })
+
+ describe('setSaveDisabled', () => {
+ test('Dispatches the correct action and payload', () => {
+ store.dispatch(headerActions.setSaveDisabled(true))
+ expect(store.getActions()).toMatchSnapshot()
+ })
+ })
+
+ describe('setDeleteDisabled', () => {
+ test('Dispatches the correct action and payload', () => {
+ store.dispatch(headerActions.setDeleteDisabled(true))
+ expect(store.getActions()).toMatchSnapshot()
+ })
+ })
+})
diff --git a/__tests__/actions/modal.test.js b/__tests__/actions/modal.test.js
new file mode 100644
index 0000000..6f774fe
--- /dev/null
+++ b/__tests__/actions/modal.test.js
@@ -0,0 +1,22 @@
+import configureStore from 'redux-mock-store'
+import { ACTIONS } from '../../app/constants/actions'
+import * as modalActions from '../../app/actions/modal'
+jest.mock('../../__mocks__/pouchdb-browser')
+
+const mockStore = configureStore()
+const store = mockStore()
+
+global.fetch = () => {}
+
+describe('modal_actions', () => {
+ beforeEach(() => {
+ store.clearActions()
+ })
+
+ describe('toggleYesNoModal', () => {
+ test('Dispatches the correct action and payload', () => {
+ store.dispatch(modalActions.toggleYesNoModal(ACTIONS.SAVE_NOTE))
+ expect(store.getActions()).toMatchSnapshot()
+ })
+ })
+})
diff --git a/__tests__/actions/note.test.js b/__tests__/actions/note.test.js
new file mode 100644
index 0000000..d63a740
--- /dev/null
+++ b/__tests__/actions/note.test.js
@@ -0,0 +1,77 @@
+import configureStore from 'redux-mock-store'
+
+// Actions to be tested
+import * as noteActions from '../../app/actions/note'
+import { NOTE_STATUS } from '../../app/constants/noteStatus'
+
+const mockStore = configureStore()
+const store = mockStore()
+
+describe('note_actions', () => {
+ beforeEach(() => {
+ store.clearActions()
+ })
+
+ describe('addNewNote', () => {
+ test('Dispatches the correct action and payload', () => {
+ store.dispatch(noteActions.addNewNote())
+ expect(store.getActions()).toMatchSnapshot()
+ })
+ })
+
+ describe('updateNoteList', () => {
+ test('Dispatches the correct action and payload', () => {
+ store.dispatch(noteActions.updateNoteList())
+ expect(store.getActions()).toMatchSnapshot()
+ })
+ })
+
+ describe('setActiveNoteIndex', () => {
+ test('Dispatches the correct action and payload', () => {
+ store.dispatch(noteActions.setActiveNoteIndex(3))
+ expect(store.getActions()).toMatchSnapshot()
+ })
+ })
+
+ describe('updateNoteTitle', () => {
+ test('Dispatches the correct action and payload', () => {
+ store.dispatch(noteActions.updateNoteTitle('Hello world'))
+ expect(store.getActions()).toMatchSnapshot()
+ })
+ })
+
+ describe('updateActiveNoteState', () => {
+ test('Dispatches the correct action and payload', () => {
+ store.dispatch(noteActions.updateActiveNoteState({}))
+ expect(store.getActions()).toMatchSnapshot()
+ })
+ })
+
+ describe('updateNoteRev', () => {
+ test('Dispatches the correct action and payload', () => {
+ store.dispatch(noteActions.updateNoteRev('a123-a123'))
+ expect(store.getActions()).toMatchSnapshot()
+ })
+ })
+
+ describe('setNoteStatus', () => {
+ test('Dispatches the correct action and payload', () => {
+ store.dispatch(noteActions.setNoteStatus(NOTE_STATUS.SAVING_NOTE))
+ expect(store.getActions()).toMatchSnapshot()
+ })
+ })
+
+ describe('loadNoteContent', () => {
+ test('Dispatches the correct action and payload', () => {
+ store.dispatch(noteActions.loadNoteContent({}))
+ expect(store.getActions()).toMatchSnapshot()
+ })
+ })
+
+ describe('deleteNoteFromList', () => {
+ test('Dispatches the correct action and payload', () => {
+ store.dispatch(noteActions.deleteNoteFromList(3))
+ expect(store.getActions()).toMatchSnapshot()
+ })
+ })
+})
diff --git a/__tests__/actions/welcome.test.js b/__tests__/actions/welcome.test.js
new file mode 100644
index 0000000..98a56f6
--- /dev/null
+++ b/__tests__/actions/welcome.test.js
@@ -0,0 +1,27 @@
+import configureStore from 'redux-mock-store'
+
+// Actions to be tested
+import * as welcomeActions from '../../app/actions/welcome'
+
+const mockStore = configureStore()
+const store = mockStore()
+
+describe('welcome_actions', () => {
+ beforeEach(() => {
+ store.clearActions()
+ })
+
+ describe('getNextLang', () => {
+ test('Dispatches the correct action and payload', () => {
+ store.dispatch(welcomeActions.getNextLang())
+ expect(store.getActions()).toMatchSnapshot()
+ })
+ })
+
+ describe('toggleFade', () => {
+ test('Dispatches the correct action and payload', () => {
+ store.dispatch(welcomeActions.toggleFade())
+ expect(store.getActions()).toMatchSnapshot()
+ })
+ })
+})
diff --git a/__tests__/components/__snapshots__/app.test.js.snap b/__tests__/components/__snapshots__/app.test.js.snap
new file mode 100644
index 0000000..6da3d4a
--- /dev/null
+++ b/__tests__/components/__snapshots__/app.test.js.snap
@@ -0,0 +1,45 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` render() renders the component 1`] = `
+
+
+
+
+
+
+
+`;
diff --git a/__tests__/components/app.test.js b/__tests__/components/app.test.js
new file mode 100644
index 0000000..e13b464
--- /dev/null
+++ b/__tests__/components/app.test.js
@@ -0,0 +1,17 @@
+import React from 'react'
+import { shallow } from 'enzyme'
+import toJson from 'enzyme-to-json'
+import App from '../../app/components/App'
+
+jest.mock('../../__mocks__/electron')
+
+describe('', () => {
+ describe('render()', () => {
+ test('renders the component', () => {
+ const wrapper = shallow()
+ const component = wrapper.dive()
+
+ expect(toJson(component)).toMatchSnapshot()
+ })
+ })
+})
diff --git a/__tests__/reducers/__snapshots__/header.test.js.snap b/__tests__/reducers/__snapshots__/header.test.js.snap
new file mode 100644
index 0000000..01ef2f9
--- /dev/null
+++ b/__tests__/reducers/__snapshots__/header.test.js.snap
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`INITIAL_STATE is correct 1`] = `
+Object {
+ "isDeleteDisabled": true,
+ "isNewNoteDisabled": false,
+ "isSaveDisabled": true,
+}
+`;
+
+exports[`UPDATE_DELETE_NOTE_STATE is correct 1`] = `
+Object {
+ "isDeleteDisabled": false,
+ "isNewNoteDisabled": false,
+ "isSaveDisabled": true,
+}
+`;
+
+exports[`UPDATE_NEW_NOTE_STATE is correct 1`] = `
+Object {
+ "isDeleteDisabled": true,
+ "isNewNoteDisabled": true,
+ "isSaveDisabled": true,
+}
+`;
+
+exports[`UPDATE_SAVE_NOTE_STATE is correct 1`] = `
+Object {
+ "isDeleteDisabled": true,
+ "isNewNoteDisabled": false,
+ "isSaveDisabled": false,
+}
+`;
diff --git a/__tests__/reducers/__snapshots__/modal.test.js.snap b/__tests__/reducers/__snapshots__/modal.test.js.snap
new file mode 100644
index 0000000..09dfcae
--- /dev/null
+++ b/__tests__/reducers/__snapshots__/modal.test.js.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`INITIAL_STATE is correct 1`] = `
+Object {
+ "showYesNoModal": false,
+ "yesNoAction": "NO_ACTION",
+}
+`;
+
+exports[`TOGGLE_SAVE_MODAL is correct 1`] = `
+Object {
+ "showYesNoModal": true,
+ "yesNoAction": "SAVE_NOTE",
+}
+`;
diff --git a/__tests__/reducers/__snapshots__/note.test.js.snap b/__tests__/reducers/__snapshots__/note.test.js.snap
new file mode 100644
index 0000000..3de987b
--- /dev/null
+++ b/__tests__/reducers/__snapshots__/note.test.js.snap
@@ -0,0 +1,85 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ADD_NOTE is correct 1`] = `
+Object {
+ "activeNoteIndex": 0,
+ "noteStatus": "NO_OPERATION",
+ "notes": Array [
+ Object {
+ "id": "id-string",
+ "rev": "",
+ "title": "",
+ },
+ ],
+}
+`;
+
+exports[`DELETE_NOTE_FROM_LIST is correct 1`] = `
+Object {
+ "activeNoteIndex": -1,
+ "noteStatus": "NO_OPERATION",
+ "notes": Array [],
+}
+`;
+
+exports[`INITIAL_STATE is correct 1`] = `
+Object {
+ "activeNoteIndex": -1,
+ "noteStatus": "NO_OPERATION",
+ "notes": Array [],
+}
+`;
+
+exports[`SET_ACTIVE_NOTE_INDEX is correct 1`] = `
+Object {
+ "activeNoteIndex": 3,
+ "noteStatus": "NO_OPERATION",
+ "notes": Array [],
+}
+`;
+
+exports[`SET_NOTE_STATUS is correct 1`] = `
+Object {
+ "activeNoteIndex": -1,
+ "noteStatus": "SAVING_NOTE",
+ "notes": Array [],
+}
+`;
+
+exports[`UPDATE_ACTIVE_NOTE_STATE is correct 1`] = `
+Object {
+ "activeNoteIndex": -1,
+ "noteStatus": "NO_OPERATION",
+ "notes": Array [],
+}
+`;
+
+exports[`UPDATE_NOTE_LIST is correct 1`] = `
+Object {
+ "activeNoteIndex": -1,
+ "noteStatus": "NO_OPERATION",
+ "notes": Array [
+ Object {
+ "id": "123",
+ "rev": "123a",
+ "title": "welcome to Deer",
+ },
+ ],
+}
+`;
+
+exports[`UPDATE_NOTE_REV is correct 1`] = `
+Object {
+ "activeNoteIndex": -1,
+ "noteStatus": "NO_OPERATION",
+ "notes": Array [],
+}
+`;
+
+exports[`UPDATE_NOTE_TITLE is correct 1`] = `
+Object {
+ "activeNoteIndex": -1,
+ "noteStatus": "NO_OPERATION",
+ "notes": Array [],
+}
+`;
diff --git a/__tests__/reducers/__snapshots__/welcome.test.js.snap b/__tests__/reducers/__snapshots__/welcome.test.js.snap
new file mode 100644
index 0000000..69ef76a
--- /dev/null
+++ b/__tests__/reducers/__snapshots__/welcome.test.js.snap
@@ -0,0 +1,74 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`GET_NEXT_LANG is correct 1`] = `
+Object {
+ "fadeIn": true,
+ "index": 0,
+}
+`;
+
+exports[`INITIAL_STATE is correct 1`] = `
+Object {
+ "fadeIn": true,
+ "index": -1,
+ "langList": Array [
+ Object {
+ "lang": "English",
+ "nextBtn": "Next",
+ "welcome": "Welcome",
+ },
+ Object {
+ "lang": "Arabic",
+ "nextBtn": "التالى",
+ "welcome": "أهلا بك",
+ },
+ Object {
+ "lang": "French",
+ "nextBtn": "Prochain",
+ "welcome": "Bienvenue",
+ },
+ Object {
+ "lang": "Spanish",
+ "nextBtn": "Siguiente",
+ "welcome": "Bienvenido",
+ },
+ Object {
+ "lang": "Italian",
+ "nextBtn": "Il prossimo",
+ "welcome": "Benvenuto",
+ },
+ Object {
+ "lang": "German",
+ "nextBtn": "Nächster",
+ "welcome": "Willkommen",
+ },
+ Object {
+ "lang": "Russian",
+ "nextBtn": "следующий",
+ "welcome": "пожаловать",
+ },
+ Object {
+ "lang": "Chinese",
+ "nextBtn": "下一个",
+ "welcome": "欢迎",
+ },
+ Object {
+ "lang": "Japanese",
+ "nextBtn": "次",
+ "welcome": "ようこそ",
+ },
+ Object {
+ "lang": "Hindi",
+ "nextBtn": "आगामी",
+ "welcome": "स्वागत हे",
+ },
+ ],
+}
+`;
+
+exports[`TOGGLE_FADE is correct 1`] = `
+Object {
+ "fadeIn": false,
+ "index": -1,
+}
+`;
diff --git a/__tests__/reducers/header.test.js b/__tests__/reducers/header.test.js
new file mode 100644
index 0000000..3257e40
--- /dev/null
+++ b/__tests__/reducers/header.test.js
@@ -0,0 +1,35 @@
+
+import headerReducer from '../../app/reducers/header'
+import { ACTIONS } from '../../app/constants/actions'
+
+describe('INITIAL_STATE', () => {
+ test('is correct', () => {
+ const action = { }
+
+ expect(headerReducer(undefined, action)).toMatchSnapshot()
+ })
+})
+
+describe('UPDATE_NEW_NOTE_STATE', () => {
+ test('is correct', () => {
+ const action = { type: ACTIONS.UPDATE_NEW_NOTE_STATE, payload: true }
+
+ expect(headerReducer(undefined, action)).toMatchSnapshot()
+ })
+})
+
+describe('UPDATE_SAVE_NOTE_STATE', () => {
+ test('is correct', () => {
+ const action = { type: ACTIONS.UPDATE_SAVE_NOTE_STATE, payload: false }
+
+ expect(headerReducer(undefined, action)).toMatchSnapshot()
+ })
+})
+
+describe('UPDATE_DELETE_NOTE_STATE', () => {
+ test('is correct', () => {
+ const action = { type: ACTIONS.UPDATE_DELETE_NOTE_STATE, payload: false }
+
+ expect(headerReducer(undefined, action)).toMatchSnapshot()
+ })
+})
diff --git a/__tests__/reducers/modal.test.js b/__tests__/reducers/modal.test.js
new file mode 100644
index 0000000..dc6d3a1
--- /dev/null
+++ b/__tests__/reducers/modal.test.js
@@ -0,0 +1,22 @@
+
+import modalReducer from '../../app/reducers/modal'
+import { ACTIONS } from '../../app/constants/actions'
+
+describe('INITIAL_STATE', () => {
+ test('is correct', () => {
+ const action = { }
+
+ expect(modalReducer(undefined, action)).toMatchSnapshot()
+ })
+})
+
+describe('TOGGLE_SAVE_MODAL', () => {
+ test('is correct', () => {
+ const action = {
+ type: ACTIONS.TOGGLE_SAVE_MODAL,
+ payload: ACTIONS.SAVE_NOTE
+ }
+
+ expect(modalReducer(undefined, action)).toMatchSnapshot()
+ })
+})
diff --git a/__tests__/reducers/note.test.js b/__tests__/reducers/note.test.js
new file mode 100644
index 0000000..1da20cb
--- /dev/null
+++ b/__tests__/reducers/note.test.js
@@ -0,0 +1,110 @@
+import noteReducer from '../../app/reducers/note'
+import { ACTIONS } from '../../app/constants/actions'
+import { NOTE_STATUS } from '../../app/constants/noteStatus'
+
+jest.mock('../../__mocks__/uuid/v4')
+
+describe('INITIAL_STATE', () => {
+ test('is correct', () => {
+ const action = { }
+
+ const result = noteReducer(undefined, action)
+ delete result.activeNoteState
+ expect(result).toMatchSnapshot()
+ })
+})
+
+describe('UPDATE_NOTE_LIST', () => {
+ test('is correct', () => {
+ const notes = [
+ {
+ doc: {
+ _id: '123',
+ _rev: '123a',
+ title: 'welcome to Deer'
+ }
+ }
+ ]
+ const action = { type: ACTIONS.UPDATE_NOTE_LIST, payload: notes }
+
+ const result = noteReducer(undefined, action)
+ delete result.activeNoteState
+ expect(result).toMatchSnapshot()
+ })
+})
+
+describe('SET_ACTIVE_NOTE_INDEX', () => {
+ test('is correct', () => {
+ const action = { type: ACTIONS.SET_ACTIVE_NOTE_INDEX, payload: 3 }
+
+ const result = noteReducer(undefined, action)
+ delete result.activeNoteState
+ expect(result).toMatchSnapshot()
+ })
+})
+
+describe('ADD_NOTE', () => {
+ test('is correct', () => {
+ const action = { type: ACTIONS.ADD_NOTE }
+
+ const result = noteReducer(undefined, action)
+ delete result.activeNoteState
+ expect(result).toMatchSnapshot()
+ })
+})
+
+describe('UPDATE_NOTE_TITLE', () => {
+ test('is correct', () => {
+ const action = { type: ACTIONS.UPDATE_NOTE_TITLE, payload: 'Hello World' }
+
+ const result = noteReducer(undefined, action)
+ delete result.activeNoteState
+ expect(result).toMatchSnapshot()
+ })
+})
+
+describe('UPDATE_NOTE_REV', () => {
+ test('is correct', () => {
+ const action = { type: ACTIONS.UPDATE_NOTE_REV, payload: 'a123-k123' }
+
+ const result = noteReducer(undefined, action)
+ delete result.activeNoteState
+ expect(result).toMatchSnapshot()
+ })
+})
+
+describe('UPDATE_ACTIVE_NOTE_STATE', () => {
+ test('is correct', () => {
+ const action = {
+ type: ACTIONS.UPDATE_ACTIVE_NOTE_STATE,
+ payload: 'new State'
+ }
+
+ const result = noteReducer(undefined, action)
+ delete result.activeNoteState
+ expect(result).toMatchSnapshot()
+ })
+})
+
+describe('SET_NOTE_STATUS', () => {
+ test('is correct', () => {
+ const action = {
+ type: ACTIONS.SET_NOTE_STATUS,
+ payload: NOTE_STATUS.SAVING_NOTE
+ }
+
+ const result = noteReducer(undefined, action)
+ delete result.activeNoteState
+ expect(result).toMatchSnapshot()
+ })
+})
+
+describe('DELETE_NOTE_FROM_LIST', () => {
+ test('is correct', () => {
+ const action = { type: ACTIONS.DELETE_NOTE_FROM_LIST }
+
+ const result = noteReducer(undefined, action)
+ delete result.activeNoteState
+ expect(result).toMatchSnapshot()
+ })
+})
diff --git a/__tests__/reducers/welcome.test.js b/__tests__/reducers/welcome.test.js
new file mode 100644
index 0000000..3e55b67
--- /dev/null
+++ b/__tests__/reducers/welcome.test.js
@@ -0,0 +1,30 @@
+import welcomeReducer from '../../app/reducers/welcome'
+import { ACTIONS } from '../../app/constants/actions'
+
+describe('INITIAL_STATE', () => {
+ test('is correct', () => {
+ const action = { }
+
+ expect(welcomeReducer(undefined, action)).toMatchSnapshot()
+ })
+})
+
+describe('GET_NEXT_LANG', () => {
+ test('is correct', () => {
+ const action = { type: ACTIONS.GET_NEXT_LANG }
+
+ const result = welcomeReducer(undefined, action)
+ delete result.langList
+ expect(result).toMatchSnapshot()
+ })
+})
+
+describe('TOGGLE_FADE', () => {
+ test('is correct', () => {
+ const action = { type: ACTIONS.TOGGLE_FADE }
+
+ const result = welcomeReducer(undefined, action)
+ delete result.langList
+ expect(result).toMatchSnapshot()
+ })
+})
diff --git a/__tests__/setup.js b/__tests__/setup.js
new file mode 100644
index 0000000..f22fd8d
--- /dev/null
+++ b/__tests__/setup.js
@@ -0,0 +1,5 @@
+import Enzyme from 'enzyme'
+import Adapter from 'enzyme-adapter-react-16'
+
+// React 16 Enzyme adapter
+Enzyme.configure({ adapter: new Adapter() })
diff --git a/app/actions/header.js b/app/actions/header.js
new file mode 100644
index 0000000..2ee7430
--- /dev/null
+++ b/app/actions/header.js
@@ -0,0 +1,11 @@
+import { createAction } from 'redux-actions'
+import { ACTIONS } from '../constants/actions'
+
+// Used for updating state of adding new note button (e.g. enabled or disabled).
+export const setNewNoteDisabled = createAction(ACTIONS.UPDATE_NEW_NOTE_STATE)
+
+// Used for updating state of save button (e.g. enabled or disabled).
+export const setSaveDisabled = createAction(ACTIONS.UPDATE_SAVE_NOTE_STATE)
+
+// Used for updating state of deleteing note button (e.g. enabled or disabled).
+export const setDeleteDisabled = createAction(ACTIONS.UPDATE_DELETE_NOTE_STATE)
diff --git a/app/actions/modal.js b/app/actions/modal.js
new file mode 100644
index 0000000..b8106bb
--- /dev/null
+++ b/app/actions/modal.js
@@ -0,0 +1,19 @@
+import { createAction } from 'redux-actions'
+import { ACTIONS } from '../constants/actions'
+import { NOTE_STATUS } from '../constants/noteStatus'
+import { setNoteStatus } from './note'
+
+// Used for updating visibility of save modal.
+export const toggleYesNoModal = createAction(ACTIONS.TOGGLE_SAVE_MODAL)
+
+// Async method, Used for updating note status with two optional parameters
+// - withTimeOut: dispatch action after timeout or not and it's default value
+// is true.
+// - status: the new status of the note and it's default value is NO_OPERATION.
+export const updateNoteStatus =
+ (withTimeOut = true, status = NOTE_STATUS.NO_OPERATION) =>
+ (dispatch, getState) => {
+ setTimeout(() => {
+ dispatch(setNoteStatus(status))
+ }, withTimeOut ? 1000 : 0)
+ }
diff --git a/app/actions/note.js b/app/actions/note.js
new file mode 100644
index 0000000..ae82ac3
--- /dev/null
+++ b/app/actions/note.js
@@ -0,0 +1,109 @@
+import { createAction } from 'redux-actions'
+import { ACTIONS } from '../constants/actions'
+import { NOTE_STATUS } from '../constants/noteStatus'
+import { addNote, fetchNotes, getNote, removeNote } from './../db'
+import { convertFromRaw, convertToRaw } from 'draft-js'
+import logger from 'electron-log'
+
+// Used for adding a new note.
+export const addNewNote = createAction(ACTIONS.ADD_NOTE)
+
+// Used for updating noteList with fetched notes from database.
+export const updateNoteList = createAction(ACTIONS.UPDATE_NOTE_LIST)
+
+// Used for setting the index of selected note.
+export const setActiveNoteIndex = createAction(ACTIONS.SET_ACTIVE_NOTE_INDEX)
+
+// Used for updating title of the active note.
+export const updateNoteTitle = createAction(ACTIONS.UPDATE_NOTE_TITLE)
+
+// Used for updating editor's state of the active note.
+export const updateActiveNoteState =
+ createAction(ACTIONS.UPDATE_ACTIVE_NOTE_STATE)
+
+// Used for updating rev of the active note.
+export const updateNoteRev = createAction(ACTIONS.UPDATE_NOTE_REV)
+
+// Used for updating status of the active note.
+export const setNoteStatus = createAction(ACTIONS.SET_NOTE_STATUS)
+
+// Used for updating status of the active note.
+export const loadNoteContent = createAction(ACTIONS.LOAD_NOTE_CONTENT)
+
+// Used for deleting a note from noteList.
+export const deleteNoteFromList = createAction(ACTIONS.DELETE_NOTE_FROM_LIST)
+
+// Async method, Used for fetching all notes from database.
+export const fetchAllNotes = () => (dispatch, getState) => {
+ fetchNotes().then((result) => {
+ // Update only if there are notes.
+ if (result.total_rows > 0) {
+ dispatch(updateNoteList(result.rows))
+ }
+ }).catch((err) => {
+ logger.error('Unable to fetch notes ' + err)
+ })
+}
+
+// Helper method, Used for setting noteStatus to NOTE_SAVE_FAIL.
+const _noteSaveFailed = (dispatch, err = null) => {
+ dispatch(setNoteStatus(NOTE_STATUS.NOTE_SAVE_FAIL))
+ logger.error('Unable to save note ' + err)
+}
+
+// Async method, Used for saving note (new or update) to database and updates
+// noteStatus.
+export const saveNote = () => (dispatch, getState) => {
+ dispatch(setNoteStatus(NOTE_STATUS.SAVING_NOTE))
+ const state = getState().noteReducer
+ const noteIndex = state.activeNoteIndex
+ const doc = {
+ _id: state.notes[noteIndex].id,
+ _rev: state.notes[noteIndex].rev,
+ title: state.notes[noteIndex].title,
+ content: convertToRaw(state.activeNoteState.getCurrentContent())
+ }
+ addNote(doc).then((result) => {
+ if (result.ok) {
+ // Update note rev to avoid conflicts while the next time for saving
+ // this note.
+ dispatch(updateNoteRev(result.rev))
+
+ dispatch(setNoteStatus(NOTE_STATUS.NOTE_SAVE_SUCCESS))
+ } else {
+ _noteSaveFailed(dispatch)
+ }
+ }).catch((err) => _noteSaveFailed(dispatch, err))
+}
+
+// Async method, Used for getting active note content from database and loads it.
+export const fetchNote = () => (dispatch, getState) => {
+ setNoteStatus(NOTE_STATUS.LOADING_NOTE)
+
+ const state = getState().noteReducer
+ const noteIndex = state.activeNoteIndex
+ getNote(state.notes[noteIndex].id).then((result) => {
+ dispatch(loadNoteContent(convertFromRaw(result.content)))
+ dispatch(setNoteStatus(NOTE_STATUS.NOTE_LOAD_SUCCESS))
+ }).catch((err) => {
+ dispatch(setNoteStatus(NOTE_STATUS.NOTE_LOAD_FAIL))
+ logger.error('Unable to get note ' + err)
+ })
+}
+
+// Async method, Used for removing active note from database.
+export const deleteNote = () => (dispatch, getState) => {
+ setNoteStatus(NOTE_STATUS.DELETING_NOTE)
+
+ const state = getState().noteReducer
+ const noteIndex = state.activeNoteIndex
+ removeNote(state.notes[noteIndex].id,
+ state.notes[noteIndex].rev).then((result) => {
+ dispatch(deleteNoteFromList(noteIndex))
+ dispatch(setActiveNoteIndex(ACTIONS.NOT_SELECTED_NOTE))
+ dispatch(setNoteStatus(NOTE_STATUS.NOTE_DELETE_SUCCESS))
+ }).catch((err) => {
+ dispatch(setNoteStatus(NOTE_STATUS.NOTE_DELETE_FAIL))
+ logger.error('Unable to remove note ' + err)
+ })
+}
diff --git a/app/assets/styles/index.css b/app/assets/styles/index.css
index 2da9207..0c5cd8f 100644
--- a/app/assets/styles/index.css
+++ b/app/assets/styles/index.css
@@ -49,7 +49,51 @@
.middle-page {
@extend .font-style-base;
- position: fixed;
- left: 35%;
+ position: sticky;
+ justify-content: center;
+ display: flex;
top: 45%;
}
+.home-container {
+ padding: 0px;
+ margin-left: -3px;
+}
+.home-sidebar {
+ position: sticky;
+ top: 48px;
+ height: calc(100vh - 48px);
+ flex: 0 1 320px;
+ -webkit-box-flex: 0;
+ border-right: 1px solid rgba(0, 0, 0, 0.125);
+ overflow: auto;
+ padding: 0px!important;
+}
+
+.home-content {
+ padding: 0px;
+}
+
+.NoteEditor {
+ font-family: 'Inconsolata', 'Menlo', 'Consolas', monospace;
+ font-size: 15px;
+ font-weight: bold;
+ color: black;
+ line-height: 30px;
+ cursor: text;
+ background: linear-gradient(transparent, transparent 28px, green 30px);
+ background-size: 30px 30px;
+ min-height: calc(100vh - 48px);
+ border-left: 3px solid #D44147;
+ padding: 5px;
+ padding-left: 10px;
+}
+
+.NoteEditor-hidePlaceholder {
+ display: none;
+}
+
+.noteItem {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
diff --git a/app/components/App.js b/app/components/App.js
index b5eb409..2cd7092 100644
--- a/app/components/App.js
+++ b/app/components/App.js
@@ -3,19 +3,17 @@ import { Switch, Route, BrowserRouter, Redirect } from 'react-router-dom'
import routes from '../routes'
export default () => (
-
-
-
- {routes.map((route, index) => (
-
- ))}
-
-
-
-
+
+
+ {routes.map((route, index) => (
+
+ ))}
+
+
+
)
diff --git a/app/components/Header.js b/app/components/Header.js
new file mode 100644
index 0000000..63a4567
--- /dev/null
+++ b/app/components/Header.js
@@ -0,0 +1,81 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { ACTIONS } from '../constants/actions'
+
+export default class Header extends Component {
+ constructor (props) {
+ // Initialize this using super
+ super()
+
+ this.onAddNote = this.onAddNote.bind(this)
+ this.onSaveNote = this.onSaveNote.bind(this)
+ this.onDeleteNote = this.onDeleteNote.bind(this)
+ }
+
+ onAddNote () {
+ // Do not proceed as button is disabled
+ if (this.props.isNewNoteDisabled) return
+
+ this.props.setNewNoteDisabled(true)
+ this.props.addNewNote()
+ }
+
+ onSaveNote () {
+ // Do not proceed as button is disabled
+ if (this.props.isSaveDisabled) return
+
+ this.props.toggleYesNoModal(ACTIONS.SAVE_NOTE)
+ }
+
+ onDeleteNote () {
+ // Do not proceed as button is disabled
+ if (this.props.isDeleteDisabled) return
+
+ this.props.toggleYesNoModal(ACTIONS.DELETE_NOTE)
+ }
+
+ render () {
+ let newNoteBtnClass = 'btn btn-outline-success btn-sm'
+ if (this.props.isNewNoteDisabled) { newNoteBtnClass += ' disabled' }
+
+ let saveBtnClass = 'ml-2 btn btn-outline-primary btn-sm'
+ if (this.props.isSaveDisabled) { saveBtnClass += ' disabled' }
+
+ let deleteBtnClass = 'ml-2 btn btn-outline-danger btn-sm'
+ if (this.props.isDeleteDisabled) { deleteBtnClass += ' disabled' }
+
+ return (
+
+ )
+ }
+}
+
+Header.propTypes = {
+ addNewNote: PropTypes.func.isRequired,
+ setNewNoteDisabled: PropTypes.func.isRequired,
+ isNewNoteDisabled: PropTypes.bool.isRequired,
+ isSaveDisabled: PropTypes.bool.isRequired,
+ isDeleteDisabled: PropTypes.bool.isRequired,
+ toggleYesNoModal: PropTypes.func.isRequired
+}
diff --git a/app/components/Home.js b/app/components/Home.js
index eb77f21..28f15af 100644
--- a/app/components/Home.js
+++ b/app/components/Home.js
@@ -1,6 +1,36 @@
import React from 'react'
-export default () => (
-
-
Deer
-
-)
+import { Redirect } from 'react-router-dom'
+import Header from './../containers/Header'
+import NoteList from './../containers/NoteList'
+import HomeContent from './../containers/HomeContent'
+import YesNoModal from './../containers/YesNoModal'
+import StatusModal from './../containers/StatusModal'
+import {
+ checkRedirectToWelcomePage,
+ setNotFirstTimeFlag
+} from '../../utils/api.electron'
+
+export default () => {
+ if (checkRedirectToWelcomePage()) {
+ setNotFirstTimeFlag()
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/app/components/HomeContent.js b/app/components/HomeContent.js
new file mode 100644
index 0000000..b7a5855
--- /dev/null
+++ b/app/components/HomeContent.js
@@ -0,0 +1,20 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import NoteEditor from './../containers/NoteEditor'
+
+export default class HomeContent extends Component {
+ render () {
+ // Show homeContent when no note is selected.
+ if (this.props.activeNoteIndex >= 0) { return () }
+
+ return (
+
+
Please select a note or add a new one
+
+ )
+ }
+}
+
+HomeContent.propTypes = {
+ activeNoteIndex: PropTypes.number.isRequired
+}
diff --git a/app/components/NoteEditor.js b/app/components/NoteEditor.js
new file mode 100644
index 0000000..3b114ff
--- /dev/null
+++ b/app/components/NoteEditor.js
@@ -0,0 +1,72 @@
+import React, { Component } from 'react'
+import { Editor, EditorState } from 'draft-js'
+import PropTypes from 'prop-types'
+
+export default class NoteEditor extends Component {
+ constructor (props) {
+ // Initialize this using super
+ super()
+
+ this.onEditorChange = this.onEditorChange.bind(this)
+ }
+
+ onEditorChange (newEditorState) {
+ const currentContent = this.props.activeNoteState.getCurrentContent()
+ const newContent = newEditorState.getCurrentContent()
+ if (currentContent !== newContent) {
+ // We only save first 40 characters of the first non-empty line if there
+ // is a change.
+ const currentText =
+ currentContent.getPlainText().trim().split('\u000A')[0].substring(0, 40)
+ const newText =
+ newContent.getPlainText().trim().split('\u000A')[0].substring(0, 40)
+ if (currentText !== newText) { this.props.updateNoteTitle(newText) }
+
+ // There is a change in content
+ if (newContent.hasText() && newText.trim()) {
+ // The new content has text, so we will enable save button.
+ this.props.setSaveDisabled(false)
+ } else {
+ // The new content is empty, so we will disable new Note and save
+ // buttons.
+ this.props.setNewNoteDisabled(true)
+ this.props.setSaveDisabled(true)
+ }
+ }
+
+ this.props.updateActiveNoteState(newEditorState)
+ }
+
+ componentDidMount () {
+ // Enable delete button as editor is mounted.
+ this.props.setDeleteDisabled(false)
+ }
+
+ componentWillUnmount () {
+ // Disable save and delete buttons as editor will be unmounted.
+ this.props.setSaveDisabled(true)
+ this.props.setDeleteDisabled(true)
+ this.props.updateActiveNoteState(EditorState.createEmpty())
+ }
+
+ render () {
+ return (
+
+
+
+ )
+ }
+}
+
+NoteEditor.propTypes = {
+ activeNoteState: PropTypes.object.isRequired,
+ setSaveDisabled: PropTypes.func.isRequired,
+ setDeleteDisabled: PropTypes.func.isRequired,
+ setNewNoteDisabled: PropTypes.func.isRequired,
+ updateActiveNoteState: PropTypes.func.isRequired,
+ updateNoteTitle: PropTypes.func.isRequired
+}
diff --git a/app/components/NoteItem.js b/app/components/NoteItem.js
new file mode 100644
index 0000000..77126df
--- /dev/null
+++ b/app/components/NoteItem.js
@@ -0,0 +1,35 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+
+export default class NoteItem extends Component {
+ constructor (props) {
+ // Initialize this using super
+ super()
+
+ this.onNoteClick = this.onNoteClick.bind(this)
+ }
+
+ onNoteClick () {
+ this.props.select(this.props.index)
+ }
+
+ render () {
+ const title = this.props.value || 'Empty note'
+
+ let classNames = 'list-group-item list-group-item-action noteItem'
+ if (this.props.isActive) { classNames += ' active text-white' }
+
+ return (
+
+ {title}
+
+ )
+ }
+}
+
+NoteItem.propTypes = {
+ index: PropTypes.number.isRequired,
+ value: PropTypes.string.isRequired,
+ select: PropTypes.func.isRequired,
+ isActive: PropTypes.bool.isRequired
+}
diff --git a/app/components/NoteList.js b/app/components/NoteList.js
new file mode 100644
index 0000000..7a6f211
--- /dev/null
+++ b/app/components/NoteList.js
@@ -0,0 +1,48 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import NoteItem from './NoteItem'
+
+export default class NoteList extends Component {
+ constructor (props) {
+ // Initialize this using super
+ super()
+
+ this.onSelect = this.onSelect.bind(this)
+ }
+
+ componentDidMount () {
+ // Trigger fetching notes as this component is loaded.
+ this.props.fetchAllNotes()
+ }
+
+ onSelect (noteIndex = -1) {
+ // Do nothing as it's already selected.
+ if (this.props.activeNoteIndex === noteIndex) { return }
+
+ this.props.setActiveNoteIndex(noteIndex)
+ this.props.fetchNote(noteIndex)
+ }
+
+ render () {
+ return (
+
+ {this.props.notes.map((note, index) => (
+
+ ))}
+
+ )
+ }
+}
+
+NoteList.propTypes = {
+ activeNoteIndex: PropTypes.number.isRequired,
+ notes: PropTypes.array.isRequired,
+ fetchAllNotes: PropTypes.func.isRequired,
+ fetchNote: PropTypes.func.isRequired,
+ setActiveNoteIndex: PropTypes.func.isRequired
+}
diff --git a/app/components/StatusModal.js b/app/components/StatusModal.js
new file mode 100644
index 0000000..aa63014
--- /dev/null
+++ b/app/components/StatusModal.js
@@ -0,0 +1,127 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { Modal, ModalBody, ModalFooter } from 'reactstrap'
+import { NOTE_STATUS } from '../constants/noteStatus'
+
+export default class StatusModal extends Component {
+ constructor (props) {
+ // Initialize this using super
+ super()
+
+ this._getModalConfig = this._getModalConfig.bind(this)
+ this.onSaveFail = this.onSaveFail.bind(this)
+ this.onLoadFail = this.onLoadFail.bind(this)
+ this.onDeleteFail = this.onDeleteFail.bind(this)
+ }
+
+ componentDidUpdate () {
+ switch (this.props.noteStatus) {
+ case NOTE_STATUS.NOTE_SAVE_SUCCESS:
+ this.props.setSaveDisabled(true)
+ this.props.setNewNoteDisabled(false)
+ this.props.updateNoteStatus()
+ break
+ case NOTE_STATUS.NOTE_LOAD_SUCCESS:
+ case NOTE_STATUS.NOTE_DELETE_SUCCESS:
+ this.props.updateNoteStatus()
+ break
+ default:
+ break
+ }
+ }
+
+ // Called when saving note is failed.
+ onSaveFail () {
+ this.props.setSaveDisabled(false)
+ this.props.updateNoteStatus(false)
+ }
+
+ // Called when loading note is failed.
+ onLoadFail () {
+ this.props.updateNoteStatus(false)
+ }
+
+ onDeleteFail () {
+ this.props.setDeleteDisabled(false)
+ this.props.updateNoteStatus(false)
+ }
+
+ _getModalConfig () {
+ // This modal is shown when any operation happens.
+ let showModal =
+ this.props.noteStatus !== NOTE_STATUS.NO_OPERATION
+
+ let modalBody = ''
+ let callBack = () => {}
+ let showFooter = false
+ switch (this.props.noteStatus) {
+ case NOTE_STATUS.SAVING_NOTE:
+ modalBody = 'Saving note...'
+ break
+ case NOTE_STATUS.NOTE_SAVE_SUCCESS:
+ modalBody = 'Note saved successfully.'
+ break
+ case NOTE_STATUS.NOTE_SAVE_FAIL:
+ modalBody = 'Unable to save note.'
+ showFooter = true
+ callBack = this.onSaveFail
+ break
+ case NOTE_STATUS.LOADING_NOTE:
+ modalBody = 'Loading note...'
+ break
+ case NOTE_STATUS.NOTE_LOAD_SUCCESS:
+ modalBody = 'Note loaded successfully.'
+ break
+ case NOTE_STATUS.NOTE_LOAD_FAIL:
+ modalBody = 'Unable to load note.'
+ showFooter = true
+ callBack = this.onLoadFail
+ break
+ case NOTE_STATUS.DELETING_NOTE:
+ modalBody = 'Deleting note...'
+ break
+ case NOTE_STATUS.NOTE_DELETE_SUCCESS:
+ modalBody = 'Note deleted successfully.'
+ break
+ case NOTE_STATUS.NOTE_DELETE_FAIL:
+ modalBody = 'Unable to delete note.'
+ showFooter = true
+ callBack = this.onDeleteFail
+ break
+ default:
+ break
+ }
+ return {
+ showModal: showModal,
+ modalBody: modalBody,
+ callBack: callBack,
+ showFooter: showFooter
+ }
+ }
+
+ render () {
+ const modalConfig = this._getModalConfig()
+ return (
+
+
+ {modalConfig.modalBody}
+
+ {modalConfig.showFooter ?
+
+ : ''}
+
+ )
+ }
+}
+
+StatusModal.propTypes = {
+ noteStatus: PropTypes.string.isRequired,
+ updateNoteStatus: PropTypes.func.isRequired,
+ setSaveDisabled: PropTypes.func.isRequired,
+ setNewNoteDisabled: PropTypes.func.isRequired,
+ setDeleteDisabled: PropTypes.func.isRequired
+}
diff --git a/app/components/YesNoModal.js b/app/components/YesNoModal.js
new file mode 100644
index 0000000..74eac88
--- /dev/null
+++ b/app/components/YesNoModal.js
@@ -0,0 +1,83 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { Modal, ModalBody, ModalFooter } from 'reactstrap'
+import { ACTIONS } from '../constants/actions'
+
+export default class YesNoModal extends Component {
+ constructor (props) {
+ // Initialize this using super
+ super()
+
+ this._getModalConfig = this._getModalConfig.bind(this)
+ this.onSaveNote = this.onSaveNote.bind(this)
+ this.onDeleteNote = this.onDeleteNote.bind(this)
+ }
+
+ // Called when saving is confirmed.
+ onSaveNote () {
+ this.props.setSaveDisabled(true)
+ this.props.toggleYesNoModal()
+ this.props.saveNote()
+ }
+
+ // Called when deleting is confirmed.
+ onDeleteNote () {
+ this.props.setDeleteDisabled(true)
+ this.props.toggleYesNoModal()
+ this.props.deleteNote()
+ }
+
+ _getModalConfig () {
+ let modalBody = ''
+ let callBack = () => {}
+ switch (this.props.yesNoAction) {
+ case ACTIONS.SAVE_NOTE:
+ modalBody = 'Do you want to save this note?'
+ callBack = this.onSaveNote
+ break
+ case ACTIONS.DELETE_NOTE:
+ modalBody = 'Do you want to delete this note?'
+ callBack = this.onDeleteNote
+ break
+ default:
+ break
+ }
+ return {
+ modalBody: modalBody,
+ callBack: callBack
+ }
+ }
+
+ render () {
+ const modalConfig = this._getModalConfig()
+ return (
+
+
+ {modalConfig.modalBody}
+
+
+
+
+
+
+ )
+ }
+}
+
+YesNoModal.propTypes = {
+ yesNoAction: PropTypes.string.isRequired,
+ showYesNoModal: PropTypes.bool.isRequired,
+ toggleYesNoModal: PropTypes.func.isRequired,
+ setSaveDisabled: PropTypes.func.isRequired,
+ setDeleteDisabled: PropTypes.func.isRequired,
+ saveNote: PropTypes.func.isRequired,
+ deleteNote: PropTypes.func.isRequired
+}
diff --git a/app/constants/actions.js b/app/constants/actions.js
index 0712304..d0356a6 100644
--- a/app/constants/actions.js
+++ b/app/constants/actions.js
@@ -1,4 +1,22 @@
export const ACTIONS = {
GET_NEXT_LANG: 'SET_NEXT_LANG',
- TOGGLE_FADE: 'TOGGLE_FADE'
+ TOGGLE_FADE: 'TOGGLE_FADE',
+ FETCH_ALL_NOTES: 'FETCH_ALL_NOTES',
+ UPDATE_NOTE_LIST: 'UPDATE_NOTE_LIST',
+ SET_ACTIVE_NOTE_INDEX: 'SET_ACTIVE_NOTE',
+ UPDATE_NEW_NOTE_STATE: 'UPDATE_NEW_NOTE_STATE',
+ UPDATE_SAVE_NOTE_STATE: 'UPDATE_SAVE_NOTE_STATE',
+ ADD_NOTE: 'ADD_NOTE',
+ UPDATE_NOTE_TITLE: 'UPDATE_NOTE_TITLE',
+ UPDATE_ACTIVE_NOTE_STATE: 'UPDATE_ACTIVE_NOTE_STATE',
+ TOGGLE_SAVE_MODAL: 'TOGGLE_SAVE_MODAL',
+ SET_NOTE_STATUS: 'SET_NOTE_STATUS',
+ UPDATE_NOTE_REV: 'UPDATE_NOTE_REV',
+ LOAD_NOTE_CONTENT: 'LOAD_NOTE_CONTENT',
+ DELETE_NOTE_FROM_LIST: 'DELETE_NOTE_FROM_LIST',
+ SAVE_NOTE: 'SAVE_NOTE',
+ DELETE_NOTE: 'DELETE_NOTE',
+ UPDATE_DELETE_NOTE_STATE: 'UPDATE_DELETE_NOTE_STATE',
+ NO_ACTION: 'NO_ACTION',
+ NOT_SELECTED_NOTE: -1
}
diff --git a/app/constants/noteStatus.js b/app/constants/noteStatus.js
new file mode 100644
index 0000000..03cda01
--- /dev/null
+++ b/app/constants/noteStatus.js
@@ -0,0 +1,12 @@
+export const NOTE_STATUS = {
+ NO_OPERATION: 'NO_OPERATION',
+ SAVING_NOTE: 'SAVING_NOTE',
+ NOTE_SAVE_SUCCESS: 'NOTE_SAVE_SUCCESS',
+ NOTE_SAVE_FAIL: 'NOTE_SAVE_FAIL',
+ LOADING_NOTE: 'LOADING_NOTE',
+ NOTE_LOAD_SUCCESS: 'NOTE_LOAD_SUCCESS',
+ NOTE_LOAD_FAIL: 'NOTE_LOAD_FAIL',
+ DELETING_NOTE: 'DELETING_NOTE',
+ NOTE_DELETE_SUCCESS: 'NOTE_DELETE_SUCCESS',
+ NOTE_DELETE_FAIL: 'NOTE_DELETE_FAIL'
+}
diff --git a/app/containers/Header.js b/app/containers/Header.js
new file mode 100644
index 0000000..74922cc
--- /dev/null
+++ b/app/containers/Header.js
@@ -0,0 +1,28 @@
+import { connect } from 'react-redux'
+import Header from '../components/Header'
+import {
+ setNewNoteDisabled
+} from '../actions/header'
+import {
+ addNewNote
+} from '../actions/note'
+import {
+ toggleYesNoModal
+} from '../actions/modal'
+
+const mapStateToProps = state => ({
+ isNewNoteDisabled: state.headerReducer.isNewNoteDisabled,
+ isSaveDisabled: state.headerReducer.isSaveDisabled,
+ isDeleteDisabled: state.headerReducer.isDeleteDisabled
+})
+
+const mapDispatchToProps = dispatch => ({
+ addNewNote: () => dispatch(addNewNote()),
+ setNewNoteDisabled: (flag) => dispatch(setNewNoteDisabled(flag)),
+ toggleYesNoModal: (action) => dispatch(toggleYesNoModal(action))
+})
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(Header)
diff --git a/app/containers/HomeContent.js b/app/containers/HomeContent.js
new file mode 100644
index 0000000..4c86a82
--- /dev/null
+++ b/app/containers/HomeContent.js
@@ -0,0 +1,14 @@
+import { connect } from 'react-redux'
+import HomeContent from '../components/HomeContent'
+
+const mapStateToProps = state => ({
+ activeNoteIndex: state.noteReducer.activeNoteIndex
+})
+
+const mapDispatchToProps = dispatch => ({
+})
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(HomeContent)
diff --git a/app/containers/NoteEditor.js b/app/containers/NoteEditor.js
new file mode 100644
index 0000000..8f19153
--- /dev/null
+++ b/app/containers/NoteEditor.js
@@ -0,0 +1,28 @@
+import { connect } from 'react-redux'
+import NoteEditor from '../components/NoteEditor'
+import {
+ setSaveDisabled,
+ setNewNoteDisabled,
+ setDeleteDisabled
+} from '../actions/header'
+import {
+ updateNoteTitle,
+ updateActiveNoteState
+} from '../actions/note'
+
+const mapStateToProps = state => ({
+ activeNoteState: state.noteReducer.activeNoteState
+})
+
+const mapDispatchToProps = dispatch => ({
+ updateActiveNoteState: (state) => dispatch(updateActiveNoteState(state)),
+ setSaveDisabled: (flag) => dispatch(setSaveDisabled(flag)),
+ setDeleteDisabled: (flag) => dispatch(setDeleteDisabled(flag)),
+ setNewNoteDisabled: (flag) => dispatch(setNewNoteDisabled(flag)),
+ updateNoteTitle: (content) => dispatch(updateNoteTitle(content))
+})
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(NoteEditor)
diff --git a/app/containers/NoteList.js b/app/containers/NoteList.js
new file mode 100644
index 0000000..325118e
--- /dev/null
+++ b/app/containers/NoteList.js
@@ -0,0 +1,23 @@
+import { connect } from 'react-redux'
+import NoteList from '../components/NoteList'
+import {
+ fetchAllNotes,
+ setActiveNoteIndex,
+ fetchNote
+} from '../actions/note'
+
+const mapStateToProps = state => ({
+ activeNoteIndex: state.noteReducer.activeNoteIndex,
+ notes: state.noteReducer.notes
+})
+
+const mapDispatchToProps = dispatch => ({
+ fetchAllNotes: () => dispatch(fetchAllNotes()),
+ fetchNote: () => dispatch(fetchNote()),
+ setActiveNoteIndex: (noteIndex) => dispatch(setActiveNoteIndex(noteIndex))
+})
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(NoteList)
diff --git a/app/containers/StatusModal.js b/app/containers/StatusModal.js
new file mode 100644
index 0000000..3637957
--- /dev/null
+++ b/app/containers/StatusModal.js
@@ -0,0 +1,27 @@
+import { connect } from 'react-redux'
+import StatusModal from '../components/StatusModal'
+import {
+ updateNoteStatus
+} from '../actions/modal'
+import {
+ setSaveDisabled,
+ setNewNoteDisabled,
+ setDeleteDisabled
+} from '../actions/header'
+
+const mapStateToProps = state => ({
+ noteStatus: state.noteReducer.noteStatus
+})
+
+const mapDispatchToProps = dispatch => ({
+ updateNoteStatus: (withTimeOut, status) =>
+ dispatch(updateNoteStatus(withTimeOut, status)),
+ setSaveDisabled: (flag) => dispatch(setSaveDisabled(flag)),
+ setNewNoteDisabled: (flag) => dispatch(setNewNoteDisabled(flag)),
+ setDeleteDisabled: (flag) => dispatch(setDeleteDisabled(flag))
+})
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(StatusModal)
diff --git a/app/containers/YesNoModal.js b/app/containers/YesNoModal.js
new file mode 100644
index 0000000..c77b9ff
--- /dev/null
+++ b/app/containers/YesNoModal.js
@@ -0,0 +1,31 @@
+import { connect } from 'react-redux'
+import YesNoModal from '../components/YesNoModal'
+import {
+ toggleYesNoModal
+} from '../actions/modal'
+import {
+ setSaveDisabled,
+ setDeleteDisabled
+} from '../actions/header'
+import {
+ saveNote,
+ deleteNote
+} from '../actions/note'
+
+const mapStateToProps = state => ({
+ showYesNoModal: state.modalReducer.showYesNoModal,
+ yesNoAction: state.modalReducer.yesNoAction
+})
+
+const mapDispatchToProps = dispatch => ({
+ toggleYesNoModal: (action) => dispatch(toggleYesNoModal(action)),
+ setSaveDisabled: (flag) => dispatch(setSaveDisabled(flag)),
+ setDeleteDisabled: (flag) => dispatch(setDeleteDisabled(flag)),
+ saveNote: () => dispatch(saveNote()),
+ deleteNote: () => dispatch(deleteNote())
+})
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(YesNoModal)
diff --git a/app/db.js b/app/db.js
index b6b1166..977982c 100644
--- a/app/db.js
+++ b/app/db.js
@@ -11,7 +11,17 @@ export const fetchNotes = () => {
})
}
-// Adds new note to database then fetches
+// Adds / Updates a note to database.
export const addNote = (doc) => {
return notesDB.put(doc)
}
+
+// Gets a note from database using its ID.
+export const getNote = (noteId) => {
+ return notesDB.get(noteId)
+}
+
+// Deletes a note from database using its ID and rev.
+export const removeNote = (noteId, noteRev) => {
+ return notesDB.remove(noteId, noteRev)
+}
diff --git a/app/reducers/header.js b/app/reducers/header.js
new file mode 100644
index 0000000..bb68099
--- /dev/null
+++ b/app/reducers/header.js
@@ -0,0 +1,30 @@
+import { ACTIONS } from '../constants/actions'
+
+const INITIAL_STATE = {
+ isNewNoteDisabled: false,
+ isSaveDisabled: true,
+ isDeleteDisabled: true
+}
+
+const _updateNoteState = (state, field, newValue) => {
+ // Just return the state as there is no change in isNewNoteDisabled flag.
+ if (state[field] === newValue) { return state }
+
+ return {
+ ...state,
+ [field]: newValue
+ }
+}
+
+export default (state = INITIAL_STATE, action) => {
+ switch (action.type) {
+ case ACTIONS.UPDATE_NEW_NOTE_STATE:
+ return _updateNoteState(state, 'isNewNoteDisabled', action.payload)
+ case ACTIONS.UPDATE_SAVE_NOTE_STATE:
+ return _updateNoteState(state, 'isSaveDisabled', action.payload)
+ case ACTIONS.UPDATE_DELETE_NOTE_STATE:
+ return _updateNoteState(state, 'isDeleteDisabled', action.payload)
+ default:
+ return state
+ }
+}
diff --git a/app/reducers/index.js b/app/reducers/index.js
index e44c74f..390f255 100644
--- a/app/reducers/index.js
+++ b/app/reducers/index.js
@@ -1,8 +1,14 @@
import { combineReducers } from 'redux'
import welcome from './welcome'
+import note from './note'
+import header from './header'
+import modal from './modal'
// Creates a single root reducer out of Deer's reducers and returns an object
// whose values are reducing functions with keys passed.
export default combineReducers({
- welcomeReducer: welcome
+ welcomeReducer: welcome,
+ noteReducer: note,
+ headerReducer: header,
+ modalReducer: modal
})
diff --git a/app/reducers/modal.js b/app/reducers/modal.js
new file mode 100644
index 0000000..0069922
--- /dev/null
+++ b/app/reducers/modal.js
@@ -0,0 +1,24 @@
+import { ACTIONS } from '../constants/actions'
+
+const INITIAL_STATE = {
+ showYesNoModal: false,
+ yesNoAction: ACTIONS.NO_ACTION
+}
+
+export default (state = INITIAL_STATE, action) => {
+ switch (action.type) {
+ case ACTIONS.TOGGLE_SAVE_MODAL:
+ let noteAction = state.yesNoAction
+ if (!state.showYesNoModal && action.payload &&
+ ACTIONS.hasOwnProperty(action.payload)) {
+ noteAction = action.payload
+ }
+ return {
+ ...state,
+ showYesNoModal: !state.showYesNoModal,
+ yesNoAction: noteAction
+ }
+ default:
+ return state
+ }
+}
diff --git a/app/reducers/note.js b/app/reducers/note.js
new file mode 100644
index 0000000..ac3bfae
--- /dev/null
+++ b/app/reducers/note.js
@@ -0,0 +1,103 @@
+import uuidv4 from 'uuid/v4'
+import { EditorState } from 'draft-js'
+import { ACTIONS } from '../constants/actions'
+import { NOTE_STATUS } from '../constants/noteStatus'
+import logger from 'electron-log'
+
+const INITIAL_STATE = {
+ activeNoteIndex: ACTIONS.NOT_SELECTED_NOTE,
+ activeNoteState: EditorState.createEmpty(),
+ noteStatus: NOTE_STATUS.NO_OPERATION,
+ notes: []
+}
+
+// Helper method, Updates a field with a newValue of an element in notes array
+// without altering provided state if there is a change in value, otherwise
+// returns provided state.
+const _updateNoteEntry = (state, field, newValue) => {
+ // Just return state as there is no active note.
+ if (state.activeNoteIndex < 0 ||
+ state.activeNoteIndex >= state.notes.length) {
+ logger.warn('action is fired with out of range' +
+ ' activeNoteIndex')
+ return state
+ }
+
+ const currentNote = Object.create(state.notes[state.activeNoteIndex])
+
+ // Just return the state if there is not a change.
+ if (currentNote[field] === newValue) { return state }
+
+ currentNote[field] = newValue
+ return {
+ ...state,
+ notes: [
+ ...state.notes.slice(0, state.activeNoteIndex),
+ currentNote,
+ ...state.notes.slice(state.activeNoteIndex + 1)
+ ]
+ }
+}
+
+export default (state = INITIAL_STATE, action) => {
+ switch (action.type) {
+ case ACTIONS.UPDATE_NOTE_LIST:
+ return {
+ ...state,
+ notes: action.payload.map(note => {
+ return {
+ id: note.doc._id,
+ rev: note.doc._rev,
+ title: note.doc.title
+ }
+ })
+ }
+ case ACTIONS.SET_ACTIVE_NOTE_INDEX:
+ return {
+ ...state,
+ activeNoteIndex: action.payload
+ }
+ case ACTIONS.ADD_NOTE:
+ return {
+ ...state,
+ notes: [...state.notes, {
+ id: uuidv4(),
+ rev: '',
+ title: ''
+ }],
+ activeNoteIndex: state.notes.length,
+ activeNoteState: EditorState.createEmpty()
+ }
+ case ACTIONS.UPDATE_NOTE_TITLE:
+ return _updateNoteEntry(state, 'title', action.payload)
+ case ACTIONS.UPDATE_NOTE_REV:
+ return _updateNoteEntry(state, 'rev', action.payload)
+ case ACTIONS.UPDATE_ACTIVE_NOTE_STATE:
+ return {
+ ...state,
+ activeNoteState: action.payload
+ }
+ case ACTIONS.SET_NOTE_STATUS:
+ if (!NOTE_STATUS.hasOwnProperty(action.payload)) {
+ logger.warn('Trying to set unsupported noteStatus: ' + action.payload)
+ return state
+ }
+
+ return {
+ ...state,
+ noteStatus: action.payload
+ }
+ case ACTIONS.LOAD_NOTE_CONTENT:
+ return {
+ ...state,
+ activeNoteState: EditorState.createWithContent(action.payload)
+ }
+ case ACTIONS.DELETE_NOTE_FROM_LIST:
+ return {
+ ...state,
+ notes: state.notes.filter(note => note.id === action.payload)
+ }
+ default:
+ return state
+ }
+}
diff --git a/appveyor.yml b/appveyor.yml
index 6c81905..661799a 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -17,6 +17,7 @@ install:
test_script:
- npm run lint
- npm run build
+ - npm run test
# Don't actually build.
build: off
diff --git a/docs/List-of-dependencies.md b/docs/List-of-dependencies.md
index af4c61e..241882f 100644
--- a/docs/List-of-dependencies.md
+++ b/docs/List-of-dependencies.md
@@ -9,22 +9,27 @@ Packages which are used during development or which are used to build Deer's bun
| babel-plugin-transform-object-rest-spread | ^6.26.0 | A Babel plugin responsible for transforming rest and spread properties for objects |
| babel-preset-env | ^1.7.0 | A Babel plugin responsible for compiling Javascript ES6 code down to ES5 |
| babel-preset-react | ^6.24.1 | A Babel plugin responsible for compiling JSX down to Javascript |
-| css-loader | ^0.28.11 | A Webpack loader responsible for collecting CSS from all the css files and put it into a string |
+| css-loader | ^1.0.0 | A Webpack loader responsible for collecting CSS from all the css files and put it into a string |
| electron | ^2.0.2 | Build cross platform desktop apps |
| electron-builder | ^20.15.1 | Package and build a ready for distribution Electron app |
| electron-reload | ^1.2.2 | Reload contents of when the source files are changed |
-| eslint | ^4.19.1 | Linting utility for JavaScript and JSX |
-| eslint-config-standard | ^11.0.0 | JavaScript Standard Style |
-| eslint-config-standard-react | ^6.0.0 | JavaScript Standard Style React/JSX support |
+| enzyme | ^3.6.0 | JavaScript Testing utilities for React |
+| enzyme-adapter-react-16 | ^1.5.0 | JavaScript Testing utilities for React |
+| enzyme-to-json | ^3.3.4 | Convert enzyme wrapper to a format compatible with Jest snapshot |
+| eslint | ^5.4.0 | Linting utility for JavaScript and JSX |
+| eslint-config-standard | ^12.0.0 | JavaScript Standard Style |
+| eslint-config-standard-react | ^7.0.2 | JavaScript Standard Style React/JSX support |
| eslint-plugin-import | ^2.12.0 | Support linting of ES2015+ (ES6+) import/export syntax |
-| eslint-plugin-node | ^6.0.1 | ESLint's rules for Node.js |
-| eslint-plugin-promise | ^3.8.0 | Enforce best practices for JavaScript promises |
+| eslint-plugin-node | ^7.0.1 | ESLint's rules for Node.js |
+| eslint-plugin-promise | ^4.0.0 | Enforce best practices for JavaScript promises |
| eslint-plugin-react | ^7.9.1 | React specific linting rules for ESLint |
-| eslint-plugin-standard | ^3.1.0 | ESlint Plugin for the Standard Linter |
-| file-loader | ^1.1.11 | A Webpack loader responsible for emitting files that will be bundled and returning their public URLs (e.g. Images) |
+| eslint-plugin-standard | ^4.0.0 | ESlint Plugin for the Standard Linter |
+| file-loader | ^2.0.0 | A Webpack loader responsible for emitting files that will be bundled and returning their public URLs (e.g. Images) |
| husky | ^0.14.3 | Prevent bad commit, push by doing checks before it takes place |
+| jest | ^23.6.0 | Delightful JavaScript Testing |
| jquery | ^3.3.1 | Needed for electron-builder packages |
-| style-loader | ^0.21.0 | A Webpack loader responsibile for taking the output string generated by by css-loader and put it inside the