From 786694d5a48181544113144d69a419a24cd8e846 Mon Sep 17 00:00:00 2001 From: "Hung Q. Le" Date: Fri, 2 Mar 2018 16:05:03 +0700 Subject: [PATCH 1/5] Refactored pouchDB helper updateDoc now take only 2 argument, the databse and the doc to be updated. This means the doc argument need to have both _id and _rev data --- app/helpers/pouchDB.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/app/helpers/pouchDB.js b/app/helpers/pouchDB.js index a519ccc2..bd313752 100644 --- a/app/helpers/pouchDB.js +++ b/app/helpers/pouchDB.js @@ -69,7 +69,7 @@ const invoicesMigrations = { placement: 'before', separator: 'commaDot', fraction: 2, - } + }, }); return newDoc; }, @@ -81,9 +81,9 @@ const invoicesMigrations = { dueDate: { selectedDate: doc.dueDate, useCustom: true, - } + }, }); - } + }, }; runMigration( @@ -176,17 +176,13 @@ const deleteDoc = (dbName, doc) => }); // Update A Document -const updateDoc = (dbName, docId, updatedDoc) => +const updateDoc = (dbName, updatedDoc) => new Promise((resolve, reject) => { setDB(dbName) .then(db => db - .get(docId) - .then(record => - db - .put(Object.assign(record, updatedDoc)) - .then(getAllDocs(dbName).then(allDocs => resolve(allDocs))) - ) + .put(updatedDoc) + .then(getAllDocs(dbName).then(allDocs => resolve(allDocs))) ) .catch(err => reject(err)); }); From 4fbd7544b163070a1f478559239aea12fc4068b9 Mon Sep 17 00:00:00 2001 From: "Hung Q. Le" Date: Fri, 2 Mar 2018 16:07:25 +0700 Subject: [PATCH 2/5] Refactored Invoice Action Creator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit + Removed (unnecessary) comments + Refactored updateInvoice action, now take only one argument – the updatedInvoice (which should contains both _id and _rev data) + Updated corresponding tests --- app/actions/__tests__/invoices.spec.js | 72 ++++++++++++++++++++++---- app/actions/invoices.jsx | 34 +++++------- 2 files changed, 76 insertions(+), 30 deletions(-) diff --git a/app/actions/__tests__/invoices.spec.js b/app/actions/__tests__/invoices.spec.js index cf30eba5..8e59411e 100644 --- a/app/actions/__tests__/invoices.spec.js +++ b/app/actions/__tests__/invoices.spec.js @@ -7,7 +7,7 @@ it('getInvoices should create GET_INVOICES action', () => { }); }); -it('saveInvoice should create SAVE_INVOICE action', () => { +it('saveInvoice should create INVOICE_SAVE action', () => { const invoiceData = { _id: 'jon_snow', fulname: 'Jon Snow', @@ -19,21 +19,73 @@ it('saveInvoice should create SAVE_INVOICE action', () => { }); }); -it('deleteInvoice should create DELETE_INVOICE action', () => { +it('newInvoiceFromContact should create INVOICE_NEW_FROM_CONTACT action', () => { + const contactData = { + _id: 'jon_snow', + fulname: 'Jon Snow', + email: 'jon@snow.got', + }; + expect(actions.newInvoiceFromContact(contactData)).toEqual({ + type: ACTION_TYPES.INVOICE_NEW_FROM_CONTACT, + payload: contactData, + }); +}); + +it('deleteInvoice should create INVOICE_DELETE action', () => { expect(actions.deleteInvoice('jon_snow')).toEqual({ type: ACTION_TYPES.INVOICE_DELETE, payload: 'jon_snow', }); }); -it('newInvoiceFromContact should create INVOICE_NEW_FROM_CONTACT action', () => { - const contact = { - id: 'abcxyz', - name: 'Jon Snow', - company: 'HBO', +it('editInvoice should create INVOICE_EDIT action', () => { + const invoiceData = { + _id: 'jon_snow', + fulname: 'Jon Snow', + email: 'jon@snow.got', }; - expect(actions.newInvoiceFromContact(contact)).toEqual({ - type: ACTION_TYPES.INVOICE_NEW_FROM_CONTACT, - payload: contact, + expect(actions.editInvoice(invoiceData)).toEqual({ + type: ACTION_TYPES.INVOICE_EDIT, + payload: invoiceData, + }); +}); + +it('updateInvoice should create INVOICE_UPDATE action', () => { + const invoiceData = { + _id: 'jon_snow', + fulname: 'Jon Snow', + email: 'jon@snow.got', + }; + expect(actions.updateInvoice(invoiceData)).toEqual({ + type: ACTION_TYPES.INVOICE_UPDATE, + payload: invoiceData, + }); +}); + +it('setInvoiceStatus should create INVOICE_SET_STATUS action', () => { + const invoiceID = 'jon_snow'; + const status = 'pending'; + expect(actions.setInvoiceStatus(invoiceID, status)).toEqual({ + type: ACTION_TYPES.INVOICE_SET_STATUS, + payload: { + invoiceID: 'jon_snow', + status: 'pending', + }, + }); +}); + +it('saveInvoiceConfigs should create INVOICE_CONFIGS_SAVE action', () => { + const invoiceID = 'jon_snow'; + const configs = { + color: 'red' + }; + expect(actions.saveInvoiceConfigs(invoiceID, configs)).toEqual({ + type: ACTION_TYPES.INVOICE_CONFIGS_SAVE, + payload: { + invoiceID: 'jon_snow', + configs: { + color: 'red' + } + } }); }); diff --git a/app/actions/invoices.jsx b/app/actions/invoices.jsx index 794fe1a3..ed3aaead 100644 --- a/app/actions/invoices.jsx +++ b/app/actions/invoices.jsx @@ -1,21 +1,23 @@ import * as ACTION_TYPES from '../constants/actions.jsx'; import { createAction } from 'redux-actions'; -// Get All Invoices export const getInvoices = createAction(ACTION_TYPES.INVOICE_GET_ALL); -// Save an Invoice export const saveInvoice = createAction( ACTION_TYPES.INVOICE_SAVE, invoiceData => invoiceData ); -export const saveInvoiceConfigs = createAction( - ACTION_TYPES.INVOICE_CONFIGS_SAVE, - (invoiceID, configs) => ({ invoiceID, configs }) +export const newInvoiceFromContact = createAction( + ACTION_TYPES.INVOICE_NEW_FROM_CONTACT, + contact => contact +); + +export const deleteInvoice = createAction( + ACTION_TYPES.INVOICE_DELETE, + invoiceID => invoiceID ); -// Edit an Invoice export const editInvoice = createAction( ACTION_TYPES.INVOICE_EDIT, invoiceData => invoiceData @@ -23,23 +25,15 @@ export const editInvoice = createAction( export const updateInvoice = createAction( ACTION_TYPES.INVOICE_UPDATE, - (invoiceID, data) => ({ invoiceID, data }) -); - -// New Invoice from Contact -export const newInvoiceFromContact = createAction( - ACTION_TYPES.INVOICE_NEW_FROM_CONTACT, - contact => contact + updatedInvoice => updatedInvoice ); -// Delete an invoice -export const deleteInvoice = createAction( - ACTION_TYPES.INVOICE_DELETE, - invoiceID => invoiceID -); - -// set the status of an invoice (pending/paid/etc.) export const setInvoiceStatus = createAction( ACTION_TYPES.INVOICE_SET_STATUS, (invoiceID, status) => ({ invoiceID, status }) ); + +export const saveInvoiceConfigs = createAction( + ACTION_TYPES.INVOICE_CONFIGS_SAVE, + (invoiceID, configs) => ({ invoiceID, configs }) +); From 9eb0f1443b373a2b5f569ec00e6cd4ce444e4535 Mon Sep 17 00:00:00 2001 From: "Hung Q. Le" Date: Fri, 2 Mar 2018 16:09:32 +0700 Subject: [PATCH 3/5] Refactored getInvoiceData helper + Make sure to retrieve the _rev data when updating the doc + Updated corresponding test --- app/helpers/__tests__/form.spec.js | 31 ++++++++++++++++++++++++++++++ app/helpers/form.js | 5 +++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/app/helpers/__tests__/form.spec.js b/app/helpers/__tests__/form.spec.js index bff311ad..24a1929b 100644 --- a/app/helpers/__tests__/form.spec.js +++ b/app/helpers/__tests__/form.spec.js @@ -2,6 +2,7 @@ import faker from 'faker'; import uuidv4 from 'uuid/v4'; import i18n from '../../../i18n/i18n'; +import omit from 'lodash'; // Helpers to test import { @@ -267,6 +268,36 @@ describe('getInvoiceData', () => { const invoiceData = getInvoiceData(newFormData); expect(invoiceData.invoiceID).toEqual('Invoice: 123-456-789'); }); + + it('should return correct metadata on editMode', () => { + const invoiceID = uuidv4(); + const invoiceRev = uuidv4(); + const createdDate = Date.now(); + const newFormData = Object.assign({}, formData, { + settings: Object.assign({}, formData.settings, { + editMode: Object.assign({}, formData.settings.editMode, { + active: true, + data: Object.assign({}, omit(formData, ['settings, savedSettings']), + { + _id: invoiceID, + _rev: invoiceRev, + created_at: createdDate + } + ) + }), + }), + }); + const invoiceData = getInvoiceData(newFormData); + expect(invoiceData._id).toEqual(invoiceID); + expect(invoiceData._rev).toEqual(invoiceRev); + expect(invoiceData.created_at).toEqual(createdDate); + }); + + // TODO + it('set status as pending when creating a new invoice'); + it('always generate _id when creating a new invoice'); + it('does not include _rev when creating a new invoice'); + it('always recalculate subTotal and grandTotal'); }); describe('validateFormData', () => { diff --git a/app/helpers/form.js b/app/helpers/form.js index fd19f286..a382cbaf 100644 --- a/app/helpers/form.js +++ b/app/helpers/form.js @@ -77,11 +77,12 @@ function getInvoiceData(formData) { // Return final value return Object.assign({}, invoiceData, { - // Reuse existing data + // Metadata _id: editMode.active ? editMode.data._id : uuidv4(), + _rev: editMode.active ? editMode.data._rev : null, created_at: editMode.active ? editMode.data.created_at : Date.now(), status: editMode.active ? editMode.data.status: 'pending', - // Calculate subtotal & grandTotal + // Alway calculate subtotal & grandTotal subtotal: getInvoiceValue(invoiceData).subtotal, grandTotal: getInvoiceValue(invoiceData).grandTotal, }); From 848a12450b5ed6c738c19a4cd6b67e2a2e599339 Mon Sep 17 00:00:00 2001 From: "Hung Q. Le" Date: Fri, 2 Mar 2018 17:12:27 +0700 Subject: [PATCH 4/5] Refactored InvoiceMiddleware + Make sure actions work with the new updateDoc in pouchDB helper + Added news tests and updated old ones + Added mock function for getSingleDoc --- app/helpers/__mocks__/pouchDB.js | 11 ++ app/middlewares/InvoicesMW.jsx | 98 +++++++-------- app/middlewares/__tests__/InvoicesMW.spec.js | 122 ++++++++++++++++++- 3 files changed, 170 insertions(+), 61 deletions(-) diff --git a/app/helpers/__mocks__/pouchDB.js b/app/helpers/__mocks__/pouchDB.js index bfac99e1..c714919f 100644 --- a/app/helpers/__mocks__/pouchDB.js +++ b/app/helpers/__mocks__/pouchDB.js @@ -62,6 +62,16 @@ const updateDoc = jest.fn( }) ); +const getSingleDoc = jest.fn( + (dbName, docID) => + new Promise((resolve, reject) => { + !dbName && reject(new Error('No database found!')); + !docID && reject(new Error('No docID found!')); + dbName === 'contacts' && resolve([...mockData.contactsRecords][0]); + dbName === 'invoices' && resolve([...mockData.invoicesRecords][0]); + }) +); + const deleteDoc = jest.fn( (dbName, docId) => new Promise((resolve, reject) => { @@ -83,6 +93,7 @@ const deleteDoc = jest.fn( module.exports = { getAllDocs, + getSingleDoc, saveDoc, updateDoc, deleteDoc, diff --git a/app/middlewares/InvoicesMW.jsx b/app/middlewares/InvoicesMW.jsx index 51474991..86bee221 100644 --- a/app/middlewares/InvoicesMW.jsx +++ b/app/middlewares/InvoicesMW.jsx @@ -79,29 +79,6 @@ const InvoicesMW = ({ dispatch, getState }) => next => action => { }); } - case ACTION_TYPES.INVOICE_CONFIGS_SAVE: { - const { invoiceID, configs } = action.payload; - return getSingleDoc('invoices', invoiceID) - .then(doc => { - dispatch({ - type: ACTION_TYPES.INVOICE_UPDATE, - payload: { - invoiceID, - data: Object.assign({}, doc, {configs}) - }, - }) - }) - .catch(err => { - next({ - type: ACTION_TYPES.UI_NOTIFICATION_NEW, - payload: { - type: 'warning', - message: err.message, - }, - }); - }); - } - case ACTION_TYPES.INVOICE_EDIT: { // Continue return getAllDocs('contacts') @@ -127,33 +104,6 @@ const InvoicesMW = ({ dispatch, getState }) => next => action => { }); } - case ACTION_TYPES.INVOICE_UPDATE: { - const { invoiceID, data } = action.payload; - return updateDoc('invoices', invoiceID, data) - .then(docs => { - next({ - type: ACTION_TYPES.INVOICE_UPDATE, - payload: docs, - }); - dispatch({ - type: ACTION_TYPES.UI_NOTIFICATION_NEW, - payload: { - type: 'success', - message: i18n.t('messages:invoice:updated'), - }, - }); - }) - .catch(err => { - next({ - type: ACTION_TYPES.UI_NOTIFICATION_NEW, - payload: { - type: 'warning', - message: err.message, - }, - }); - }); - } - case ACTION_TYPES.INVOICE_DELETE: { return deleteDoc('invoices', action.payload) .then(remainingDocs => { @@ -188,13 +138,11 @@ const InvoicesMW = ({ dispatch, getState }) => next => action => { }); } - case ACTION_TYPES.INVOICE_SET_STATUS: { - return updateDoc('invoices', action.payload.invoiceID, { - status: action.payload.status, - }) + case ACTION_TYPES.INVOICE_UPDATE: { + return updateDoc('invoices', action.payload) .then(docs => { next({ - type: ACTION_TYPES.INVOICE_SET_STATUS, + type: ACTION_TYPES.INVOICE_UPDATE, payload: docs, }); dispatch({ @@ -216,6 +164,46 @@ const InvoicesMW = ({ dispatch, getState }) => next => action => { }); } + case ACTION_TYPES.INVOICE_CONFIGS_SAVE: { + const { invoiceID, configs } = action.payload; + return getSingleDoc('invoices', invoiceID) + .then(doc => { + dispatch({ + type: ACTION_TYPES.INVOICE_UPDATE, + payload: Object.assign({}, doc, {configs}) + }) + }) + .catch(err => { + next({ + type: ACTION_TYPES.UI_NOTIFICATION_NEW, + payload: { + type: 'warning', + message: err.message, + }, + }); + }); + } + + case ACTION_TYPES.INVOICE_SET_STATUS: { + const { invoiceID, status } = action.payload; + return getSingleDoc('invoices', invoiceID) + .then(doc => { + dispatch({ + type: ACTION_TYPES.INVOICE_UPDATE, + payload: Object.assign({}, doc, { status }) + }) + }) + .catch(err => { + next({ + type: ACTION_TYPES.UI_NOTIFICATION_NEW, + payload: { + type: 'warning', + message: err.message, + }, + }); + }); + } + default: { return next(action); } diff --git a/app/middlewares/__tests__/InvoicesMW.spec.js b/app/middlewares/__tests__/InvoicesMW.spec.js index a5eefd9b..4707751f 100644 --- a/app/middlewares/__tests__/InvoicesMW.spec.js +++ b/app/middlewares/__tests__/InvoicesMW.spec.js @@ -8,9 +8,10 @@ import i18n from '../../../i18n/i18n'; // Mock Functions const { getAllDocs, - updateDoc, + getSingleDoc, saveDoc, deleteDoc, + updateDoc, mockData, } = require('../../helpers/pouchDB'); import { getInvoiceValue } from '../../helpers/invoice'; @@ -20,12 +21,15 @@ jest.mock('../../helpers/invoice'); Date.now = jest.fn(() => 'now'); describe('Invoices Middleware', () => { - let next, dispatch, middleware; + let next, dispatch, middleware, getState; beforeEach(() => { next = jest.fn(); dispatch = jest.fn(); - middleware = InvoicesMW({ dispatch })(next); + getState = jest.fn(() => ({ + form: { settings: { editMode: { active: false } } } + })); + middleware = InvoicesMW({ dispatch, getState })(next); }); describe('should handle INVOICE_GET_ALL action', () => { @@ -286,8 +290,7 @@ describe('Invoices Middleware', () => { }); }) - // TODO - describe('should handle INVOICE_UDPATE action', () => { + describe('should handle INVOICE_UPDATE action', () => { let currentInvoice; beforeEach(() => { currentInvoice = { @@ -365,7 +368,6 @@ describe('Invoices Middleware', () => { }); }) - describe('should handle INVOICE_DELETE action', () => { it('should remove record from DB correctly', () => { const invoiceID = 'jon-invoice'; @@ -404,6 +406,39 @@ describe('Invoices Middleware', () => { ); }); + it('should clear the form if this invoice is being editted', () => { + const getState = jest.fn(() => ({ + form: { + settings: { + editMode: { + active: true, + data: { _id: 'jon-invoice' } + } + } + } + })); + const middleware = InvoicesMW({ dispatch, getState })(next); + const invoiceID = 'jon-invoice'; + // Execute + middleware(Actions.deleteInvoice(invoiceID)).then(() => + deleteDoc('invoices', invoiceID).then(data => { + expect(dispatch.mock.calls.length).toBe(2); + // Dispatch success notification + expect(dispatch.mock.calls[0][0]).toEqual({ + type: ACTION_TYPES.UI_NOTIFICATION_NEW, + payload: { + type: 'success', + message: i18n.t('messages:invoice:deleted'), + }, + }); + // Dispatch clear Form action + expect(dispatch.mock.calls[1][0]).toEqual({ + type: ACTION_TYPES.FORM_CLEAR + }); + }) + ); + }) + it('handle error correctly', () => { // Setup const invoiceID = 'ned-stark'; @@ -423,6 +458,80 @@ describe('Invoices Middleware', () => { }); }); + describe('should handle INVOICE_CONFIGS_SAVE action correctly', () => { + it('get the doc, merge with config object and dispatch a new action', () => { + const invoiceID = 'id-string'; + const configs = { color: 'red' }; + middleware(Actions.saveInvoiceConfigs(invoiceID, configs)).then(() => + getSingleDoc('invoices', invoiceID).then(doc => { + expect(dispatch.mock.calls.length).toBe(1); + expect(dispatch).toHaveBeenCalledWith({ + type: ACTION_TYPES.INVOICE_UPDATE, + payload: Object.assign({}, doc, { configs }), + }); + }) + ); + }); + + it('handle error correctly', () => { + // Setup + const invoiceID = 'id-string'; + const configs = { color: 'red' }; + const expectedError = new Error('No invoice found!'); + // Execute + middleware(Actions.saveInvoiceConfigs(invoiceID, configs)).then(() => { + getSingleDoc('test', invoiceID).then(() => { + // Expect + expect(next).toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith({ + type: ACTION_TYPES.UI_NOTIFICATION_NEW, + payload: { + type: 'warning', + message: expectedError.message, + }, + }); + }) + }); + }); + }) + + describe('should handle INVOICE_SET_STATUS action', () => { + it('get the doc, merge with status object and dispatch a new action', () => { + const invoiceID = 'id-string'; + const status = 'paid'; + middleware(Actions.setInvoiceStatus(invoiceID, status)).then(() => + getSingleDoc('invoices', invoiceID).then(doc => { + expect(dispatch.mock.calls.length).toBe(1); + expect(dispatch).toHaveBeenCalledWith({ + type: ACTION_TYPES.INVOICE_UPDATE, + payload: Object.assign({}, doc, { status }), + }); + }) + ); + }); + + it('handle error correctly', () => { + // Setup + const invoiceID = 'id-string'; + const status = 'paid'; + const expectedError = new Error('No invoice found!'); + // Execute + middleware(Actions.setInvoiceStatus(invoiceID, status)).then(() => { + getSingleDoc('test', invoiceID).then(() => { + // Expect + expect(next).toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith({ + type: ACTION_TYPES.UI_NOTIFICATION_NEW, + payload: { + type: 'warning', + message: expectedError.message, + }, + }); + }) + }); + }); + }); + it('should handle INVOICE_NEW_FROM_CONTACT action', () => { const contact = { id: 'jon-snow', @@ -454,4 +563,5 @@ describe('Invoices Middleware', () => { middleware(action); expect(next).toHaveBeenCalledWith(action); }); + }); From 90c1450ffe861e02818692a956225bfa216b830d Mon Sep 17 00:00:00 2001 From: "Hung Q. Le" Date: Fri, 2 Mar 2018 17:13:00 +0700 Subject: [PATCH 5/5] Updated FormMW Make sure updateInvoice now take only one argument --- app/middlewares/FormMW.jsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/middlewares/FormMW.jsx b/app/middlewares/FormMW.jsx index 36e3a11b..20bed73c 100644 --- a/app/middlewares/FormMW.jsx +++ b/app/middlewares/FormMW.jsx @@ -23,15 +23,14 @@ const FormMW = ({ dispatch, getState }) => next => action => { // Validate Form Data if (!validateFormData(currentFormData)) return; const currentInvoiceData = getInvoiceData(currentFormData); - // Check Edit Mode + // UPDATE DOC if (currentFormData.settings.editMode.active) { - const invoiceId = currentFormData.settings.editMode.data._id; // Update existing invoice - dispatch(InvoicesActions.updateInvoice(invoiceId, currentInvoiceData)); + dispatch(InvoicesActions.updateInvoice(currentInvoiceData)); // Change Tab to invoices dispatch(UIActions.changeActiveTab('invoices')); } else { - // Save Invoice To DB + // CREATE DOC dispatch(InvoicesActions.saveInvoice(currentInvoiceData)); } // Save Contact to DB if it's a new one