From fa8c37065bca31b6aaec2d04503d27a2618f8f68 Mon Sep 17 00:00:00 2001 From: "Hung Q. Le" Date: Tue, 16 Jan 2018 23:45:50 +0700 Subject: [PATCH 1/8] Added edit & update action for invoice --- app/actions/invoices.jsx | 11 +++++++++++ app/constants/actions.jsx | 2 ++ app/reducers/InvoicesReducer.jsx | 1 + 3 files changed, 14 insertions(+) diff --git a/app/actions/invoices.jsx b/app/actions/invoices.jsx index 2b4e05b2..0868f9bc 100644 --- a/app/actions/invoices.jsx +++ b/app/actions/invoices.jsx @@ -10,6 +10,17 @@ export const saveInvoice = createAction( invoiceData => invoiceData ); +// Edit an Invoice +export const editInvoice = createAction( + ACTION_TYPES.INVOICE_EDIT, + invoiceData => invoiceData +); + +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, diff --git a/app/constants/actions.jsx b/app/constants/actions.jsx index bb5ee425..f2dfeeb7 100644 --- a/app/constants/actions.jsx +++ b/app/constants/actions.jsx @@ -20,6 +20,8 @@ export const SAVED_FORM_SETTING_UPDATE = 'SAVED_FORM_SETTING_UPDATE'; // INVOICE // =========================================================== +export const INVOICE_EDIT = 'INVOICE_EDIT'; +export const INVOICE_UPDATE = 'INVOICE_UPDATE'; export const INVOICE_SAVE = 'INVOICE_SAVE'; export const INVOICE_DELETE = 'INVOICE_DELETE'; export const INVOICE_GET_ALL = 'INVOICE_GET_ALL'; diff --git a/app/reducers/InvoicesReducer.jsx b/app/reducers/InvoicesReducer.jsx index 1050c091..d0435fbe 100644 --- a/app/reducers/InvoicesReducer.jsx +++ b/app/reducers/InvoicesReducer.jsx @@ -7,6 +7,7 @@ const InvoicesReducer = handleActions( [combineActions( Actions.getInvoices, Actions.saveInvoice, + Actions.updateInvoice, Actions.deleteInvoice, Actions.setInvoiceStatus )]: (state, action) => action.payload, From 3a3530d37e1d4f3e97fc59c6c7f87d21f6c349fe Mon Sep 17 00:00:00 2001 From: "Hung Q. Le" Date: Tue, 16 Jan 2018 23:46:23 +0700 Subject: [PATCH 2/8] Map edit invoice action to component --- app/components/invoices/Invoice.jsx | 15 +++++---------- app/containers/Invoices.jsx | 7 +++++++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/components/invoices/Invoice.jsx b/app/components/invoices/Invoice.jsx index 84999f2c..8c222446 100644 --- a/app/components/invoices/Invoice.jsx +++ b/app/components/invoices/Invoice.jsx @@ -1,5 +1,5 @@ // Libs -import React, { Component } from 'react'; +import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { truncate } from 'lodash'; import styled from 'styled-components'; @@ -142,7 +142,7 @@ const Field = styled.div` `; // Component -class Invoice extends Component { +class Invoice extends PureComponent { constructor(props) { super(props); this.viewInvoice = this.viewInvoice.bind(this); @@ -151,20 +151,14 @@ class Invoice extends Component { this.displayStatus = this.displayStatus.bind(this); } - shouldComponentUpdate(nextProps) { - if (this.props.invoice.status !== nextProps.invoice.status) { - return true; - } - return false; - } - deleteInvoice() { const { invoice, deleteInvoice } = this.props; deleteInvoice(invoice._id); } editInvoice() { - // TODO + const { invoice, editInvoice } = this.props; + editInvoice(invoice); } viewInvoice() { @@ -297,6 +291,7 @@ class Invoice extends Component { } Invoice.propTypes = { + editInvoice: PropTypes.func.isRequired, deleteInvoice: PropTypes.func.isRequired, invoice: PropTypes.object.isRequired, setInvoiceStatus: PropTypes.func.isRequired, diff --git a/app/containers/Invoices.jsx b/app/containers/Invoices.jsx index 379c3766..6d3103d0 100644 --- a/app/containers/Invoices.jsx +++ b/app/containers/Invoices.jsx @@ -27,6 +27,7 @@ import { class Invoices extends PureComponent { constructor(props) { super(props); + this.editInvoice = this.editInvoice.bind(this); this.deleteInvoice = this.deleteInvoice.bind(this); this.setInvoiceStatus = this.setInvoiceStatus.bind(this); } @@ -72,6 +73,11 @@ class Invoices extends PureComponent { dispatch(Actions.setInvoiceStatus(invoiceId, status)); } + editInvoice(invoice) { + const { dispatch } = this.props; + dispatch(Actions.editInvoice(invoice)); + } + // Render render() { const { invoices, dateFormat } = this.props; @@ -79,6 +85,7 @@ class Invoices extends PureComponent { Date: Tue, 16 Jan 2018 23:46:51 +0700 Subject: [PATCH 3/8] Switch Text from Save <-> Update when in edit mode --- app/containers/Form.jsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/containers/Form.jsx b/app/containers/Form.jsx index 54ead95c..d0fd94f2 100644 --- a/app/containers/Form.jsx +++ b/app/containers/Form.jsx @@ -57,7 +57,7 @@ class Form extends Component { settings, savedSettings, } = this.props.currentInvoice; - const { required_fields, open } = settings; + const { required_fields, open, editMode } = settings; return ( @@ -66,8 +66,12 @@ class Form extends Component { - From 97662f1f23515c77df39b48e45e995854bc9bbe3 Mon Sep 17 00:00:00 2001 From: "Hung Q. Le" Date: Tue, 16 Jan 2018 23:48:18 +0700 Subject: [PATCH 4/8] Handle INVOICE_EDIT & INVOICE_UPDATE actions in middleware --- app/middlewares/FormMW.jsx | 20 +++++++++++------ app/middlewares/InvoicesMW.jsx | 39 ++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/app/middlewares/FormMW.jsx b/app/middlewares/FormMW.jsx index fb69cb73..dacd6c81 100644 --- a/app/middlewares/FormMW.jsx +++ b/app/middlewares/FormMW.jsx @@ -10,6 +10,7 @@ import * as FormActions from '../actions/form'; import * as InvoicesActions from '../actions/invoices'; import * as ContactsActions from '../actions/contacts'; import * as SettingsActions from '../actions/settings'; +import * as UIActions from '../actions/ui'; // Helper import { getInvoiceData, validateFormData } from '../helpers/form'; @@ -20,13 +21,20 @@ const FormMW = ({ dispatch, getState }) => next => action => { const currentFormData = getState().form; // Validate Form Data if (!validateFormData(currentFormData)) return; - // Save Invoice To DB const currentInvoiceData = getInvoiceData(currentFormData); - dispatch(InvoicesActions.saveInvoice(currentInvoiceData)); - // Save Contact to DB if it's a new one - if (currentFormData.recipient.newRecipient) { - const newContactData = currentFormData.recipient.new; - dispatch(ContactsActions.saveContact(newContactData)); + // Check Edit Mode + if (currentFormData.settings.editMode.active) { + const invoiceId = currentFormData.settings.editMode.data._id; + dispatch(InvoicesActions.updateInvoice(invoiceId, currentInvoiceData)); + dispatch(UIActions.changeActiveTab('invoices')); + } else { + // Save Invoice To DB + dispatch(InvoicesActions.saveInvoice(currentInvoiceData)); + // Save Contact to DB if it's a new one + if (currentFormData.recipient.newRecipient) { + const newContactData = currentFormData.recipient.new; + dispatch(ContactsActions.saveContact(newContactData)); + } } // Clear The Form dispatch(FormActions.clearForm(null, true)); diff --git a/app/middlewares/InvoicesMW.jsx b/app/middlewares/InvoicesMW.jsx index eca17268..ac7f2db0 100644 --- a/app/middlewares/InvoicesMW.jsx +++ b/app/middlewares/InvoicesMW.jsx @@ -26,6 +26,7 @@ const InvoicesMW = ({ dispatch }) => next => action => { newRecipient: false, }) ); + break; } case ACTION_TYPES.INVOICE_GET_ALL: { @@ -89,6 +90,44 @@ const InvoicesMW = ({ dispatch }) => next => action => { }); } + case ACTION_TYPES.INVOICE_EDIT: { + // Continue + next(action); + // Change Tab to Form + dispatch(UIActions.changeActiveTab('form')); + break; + } + + case ACTION_TYPES.INVOICE_UPDATE: { + return updateDoc('invoices', action.payload.invoiceID, { + ...action.payload.data, + subtotal: getInvoiceValue(action.payload.data).subtotal, + grandTotal: getInvoiceValue(action.payload.data).grandTotal, + }) + .then(docs => { + next({ + type: ACTION_TYPES.INVOICE_UPDATE, + payload: docs, + }); + dispatch({ + type: ACTION_TYPES.UI_NOTIFICATION_NEW, + payload: { + type: 'success', + message: 'Updated Successfully', + }, + }); + }) + .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 => { From 1f4079407436d4d6017a9533ea48eaa58653f142 Mon Sep 17 00:00:00 2001 From: "Hung Q. Le" Date: Tue, 16 Jan 2018 23:48:39 +0700 Subject: [PATCH 5/8] Updated Reducer --- app/reducers/FormReducer.jsx | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/app/reducers/FormReducer.jsx b/app/reducers/FormReducer.jsx index 9e36990d..0015cb6e 100644 --- a/app/reducers/FormReducer.jsx +++ b/app/reducers/FormReducer.jsx @@ -24,6 +24,9 @@ const initialState = { // Form current settings settings: { open: false, + editMode: { + active: false + }, required_fields: invoiceSettings.required_fields, }, // Saved settings, reserve for reference @@ -102,6 +105,49 @@ const FormReducer = handleActions( }), }), + [ACTION_TYPES.INVOICE_EDIT]: (state, action) => { + const { + recipient, + rows, + currency, + tax, + dueDate, + discount, + note, + } = action.payload; + return Object.assign({}, state, { + // Populate data + recipient: Object.assign({}, state.recipient, { + newRecipient: false, + select: recipient, + }), + rows, + currency, + dueDate: dueDate !== undefined ? Object.assign({}, state.dueDate, { + selectedDate: dueDate + }) : state.dueDate, + discount: discount !== undefined ? discount : state.discount, + tax: tax !== undefined ? tax : state.tax, + note: note !== undefined ? Object.assign({}, state.note, { + content: note + }) : state.note, + // Update settings + settings: Object.assign({}, state.settings, { + editMode: { + active: true, + data: action.payload, + }, + required_fields: Object.assign({}, state.settings.required_fields, { + currency: currency.code !== state.savedSettings.currency, + tax: tax !== undefined, + dueDate: dueDate !== undefined, + discount: discount !== undefined, + note: note !== undefined, + }) + }), + }); + }, + [ACTION_TYPES.SAVED_FORM_SETTING_UPDATE]: (state, action) => { const invoiceSettings = action.payload; return Object.assign({}, state, { @@ -122,6 +168,9 @@ const FormReducer = handleActions( // Update current settings settings: Object.assign({}, state.settings, { open: false, + editMode: { + active: false + }, required_fields: state.savedSettings.required_fields, }), // Updated saved settings to the current saved settings From 85498d0afca22a6c50b3e8b6e88a824f28c59e51 Mon Sep 17 00:00:00 2001 From: "Hung Q. Le" Date: Tue, 16 Jan 2018 23:48:59 +0700 Subject: [PATCH 6/8] Handle form reset action on Discount component --- app/components/form/Discount.jsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/components/form/Discount.jsx b/app/components/form/Discount.jsx index 85f6668c..11206cc4 100644 --- a/app/components/form/Discount.jsx +++ b/app/components/form/Discount.jsx @@ -1,6 +1,7 @@ // Libraries import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import { isEmpty } from 'lodash'; // Custom Components import { Section } from '../shared/Section'; @@ -42,6 +43,17 @@ export class Discount extends Component { }); } + // Handle Reset Form + componentWillReceiveProps(nextProps) { + const { discount } = nextProps; + if (isEmpty(discount)) { + this.setState({ + amount: '', + type: 'percentage', + }); + } + } + shouldComponentUpdate(nextProps, nextState) { return ( this.state !== nextState || this.props.discount !== nextProps.discount From 7ca14cb5e93c4b56a9d5754b58b15fa2ad0c6e28 Mon Sep 17 00:00:00 2001 From: "Hung Q. Le" Date: Wed, 17 Jan 2018 08:52:11 +0700 Subject: [PATCH 7/8] Allow creating new contact in edit mode --- app/middlewares/FormMW.jsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/middlewares/FormMW.jsx b/app/middlewares/FormMW.jsx index dacd6c81..d04ec81f 100644 --- a/app/middlewares/FormMW.jsx +++ b/app/middlewares/FormMW.jsx @@ -25,16 +25,18 @@ const FormMW = ({ dispatch, getState }) => next => action => { // Check Edit Mode if (currentFormData.settings.editMode.active) { const invoiceId = currentFormData.settings.editMode.data._id; + // Update existing invoice dispatch(InvoicesActions.updateInvoice(invoiceId, currentInvoiceData)); + // Change Tab to invoices dispatch(UIActions.changeActiveTab('invoices')); } else { // Save Invoice To DB dispatch(InvoicesActions.saveInvoice(currentInvoiceData)); - // Save Contact to DB if it's a new one - if (currentFormData.recipient.newRecipient) { - const newContactData = currentFormData.recipient.new; - dispatch(ContactsActions.saveContact(newContactData)); - } + } + // Save Contact to DB if it's a new one + if (currentFormData.recipient.newRecipient) { + const newContactData = currentFormData.recipient.new; + dispatch(ContactsActions.saveContact(newContactData)); } // Clear The Form dispatch(FormActions.clearForm(null, true)); From 3cd5be616024a04865cfb9337f30e337e8e55a88 Mon Sep 17 00:00:00 2001 From: "Hung Q. Le" Date: Wed, 17 Jan 2018 22:51:32 +0700 Subject: [PATCH 8/8] Fixed Tests --- .../invoices/__tests__/Invoice.spec.js | 15 ++ app/helpers/__mocks__/pouchDB.js | 12 + app/middlewares/InvoicesMW.jsx | 2 +- app/middlewares/__tests__/FormMW.spec.js | 64 ++++++ app/middlewares/__tests__/InvoicesMW.spec.js | 117 ++++++++++ app/reducers/FormReducer.jsx | 24 +- app/reducers/__tests__/FormReducer.spec.js | 205 ++++++++++++++++++ 7 files changed, 429 insertions(+), 10 deletions(-) diff --git a/app/components/invoices/__tests__/Invoice.spec.js b/app/components/invoices/__tests__/Invoice.spec.js index 9c68b1cd..d797b4bd 100644 --- a/app/components/invoices/__tests__/Invoice.spec.js +++ b/app/components/invoices/__tests__/Invoice.spec.js @@ -32,6 +32,7 @@ const invoice = { }, }; +const editInvoice = jest.fn(); const deleteInvoice = jest.fn(); const setInvoiceStatus = jest.fn(); const dateFormat = 'MM/DD/YY'; @@ -43,6 +44,7 @@ describe('Renders correctly to the DOM', () => { wrapper = shallow( { const mountWrapper = mount( { const paidInvoiceWapper = shallow( { const cancelledInvoiceWapper = shallow( { const refundedInvoiceWapper = shallow( { .create( { wrapper = shallow( { deleteBtn = wrapper.find(Button).first(); }); + // TODO + it('handle edit action correctly', () => { + editBtn.simulate('click'); + expect(editInvoice).toHaveBeenCalled(); + expect(editInvoice).toHaveBeenCalledWith(invoice); + }); + it('handle delete action correctly', () => { deleteBtn.simulate('click'); expect(deleteInvoice).toHaveBeenCalled(); diff --git a/app/helpers/__mocks__/pouchDB.js b/app/helpers/__mocks__/pouchDB.js index a9eb5307..bfac99e1 100644 --- a/app/helpers/__mocks__/pouchDB.js +++ b/app/helpers/__mocks__/pouchDB.js @@ -52,6 +52,16 @@ const saveDoc = jest.fn( }) ); +const updateDoc = jest.fn( + (dbName, doc) => + new Promise((resolve, reject) => { + !dbName && reject(new Error('No database found!')); + !doc && reject(new Error('No doc found!')); + dbName === 'contacts' && resolve([...mockData.contactsRecords]); + dbName === 'invoices' && resolve([...mockData.invoicesRecords]); + }) +); + const deleteDoc = jest.fn( (dbName, docId) => new Promise((resolve, reject) => { @@ -70,9 +80,11 @@ const deleteDoc = jest.fn( }) ); + module.exports = { getAllDocs, saveDoc, + updateDoc, deleteDoc, mockData, }; diff --git a/app/middlewares/InvoicesMW.jsx b/app/middlewares/InvoicesMW.jsx index ac7f2db0..81cc149d 100644 --- a/app/middlewares/InvoicesMW.jsx +++ b/app/middlewares/InvoicesMW.jsx @@ -113,7 +113,7 @@ const InvoicesMW = ({ dispatch }) => next => action => { type: ACTION_TYPES.UI_NOTIFICATION_NEW, payload: { type: 'success', - message: 'Updated Successfully', + message: 'Invoice Updated Successfully', }, }); }) diff --git a/app/middlewares/__tests__/FormMW.spec.js b/app/middlewares/__tests__/FormMW.spec.js index 2a1b60dd..cd266fdd 100644 --- a/app/middlewares/__tests__/FormMW.spec.js +++ b/app/middlewares/__tests__/FormMW.spec.js @@ -18,6 +18,11 @@ describe('Form Middleware', () => { form: { validation: true, recipient: { newRecipient: true }, + settings: { + editMode: { + active: false, + } + } }, })); const middleware = FormMW({ dispatch, getState })(next); @@ -40,6 +45,11 @@ describe('Form Middleware', () => { form: { validation: true, recipient: { newRecipient: false }, + settings: { + editMode: { + active: false, + } + } }, })); const middleware = FormMW({ dispatch, getState })(next); @@ -55,6 +65,60 @@ describe('Form Middleware', () => { // No Calling Next expect(next.mock.calls.length).toBe(0); }); + + it('should update Invocie, save Contact, Clear the Form and Switch Tab', () => { + // Setup + getState = jest.fn(() => ({ + form: { + validation: true, + recipient: { newRecipient: true }, + settings: { + editMode: { + active: true, + data: { _id: 'invoice-uuid' } + } + } + }, + })); + const middleware = FormMW({ dispatch, getState })(next); + const action = Actions.saveFormData(); + // Action + middleware(action); + // Expect + expect(getState.mock.calls.length).toBe(1); + // Update the Invoice, Save new contact, Clear the Form & Change Tab + expect(dispatch.mock.calls.length).toBe(4); + // No Calling Next + expect(next.mock.calls.length).toBe(0); + }); + + it('should update Invocie, NOT add new Contact, Clear the Form and Switch Tab', () => { + // Setup + getState = jest.fn(() => ({ + form: { + validation: true, + recipient: { newRecipient: false }, + settings: { + editMode: { + active: true, + data: { _id: 'invoice-uuid' } + } + } + }, + })); + const middleware = FormMW({ dispatch, getState })(next); + + // Action + const action = Actions.saveFormData(); + middleware(action); + + // Expect + expect(getState.mock.calls.length).toBe(1); + // Update the Invoice, Clear the Form and Switch Tab + expect(dispatch.mock.calls.length).toBe(3); + // No Calling Next + expect(next.mock.calls.length).toBe(0); + }); }); it('should NOT pass validation', () => { diff --git a/app/middlewares/__tests__/InvoicesMW.spec.js b/app/middlewares/__tests__/InvoicesMW.spec.js index e8beb65e..ce84b24f 100644 --- a/app/middlewares/__tests__/InvoicesMW.spec.js +++ b/app/middlewares/__tests__/InvoicesMW.spec.js @@ -7,6 +7,7 @@ import uuidv4 from 'uuid/v4'; // Mock Functions const { getAllDocs, + updateDoc, saveDoc, deleteDoc, mockData, @@ -278,6 +279,122 @@ describe('Invoices Middleware', () => { }); }); + describe('should handle INVOICE_EDIT action', () => { + it('should call next and dispatch change Tab action', () => { + // Setup + const currentInvoice = { + recipient: { + fullname: faker.name.findName(), + email: faker.internet.email(), + }, + currency: { + code: 'USD', + symbol: '$', + }, + rows: [ + { + id: uuidv4(), + description: faker.commerce.productName(), + price: faker.commerce.price(), + quantity: faker.random.number(10), + }, + ], + }; + // Execute + const action = Actions.editInvoice(currentInvoice); + middleware(action); + // Call next + expect(next.mock.calls.length).toBe(1); + expect(next).toHaveBeenCalledWith(action); + // Dispatch change Tab action + expect(dispatch.mock.calls.length).toBe(1); + expect(dispatch).toHaveBeenCalledWith({ + type: ACTION_TYPES.UI_TAB_CHANGE, + payload: 'form' + }); + }); + }) + + // TODO + describe('should handle INVOICE_UDPATE action', () => { + let currentInvoice; + beforeEach(() => { + currentInvoice = { + recipient: { + fullname: faker.name.findName(), + email: faker.internet.email(), + }, + currency: { + code: 'USD', + symbol: '$', + }, + rows: [ + { + id: uuidv4(), + description: faker.commerce.productName(), + price: faker.commerce.price(), + quantity: faker.random.number(10), + }, + ], + }; + }); + + it('should update the invoice', () => { + middleware(Actions.updateInvoice(currentInvoice)).then(() => + updateDoc('invoices', currentInvoice).then(data => { + expect(data).toEqual(mockData.invoicesRecords); + }) + ); + }); + + it('should call next and dispatch notification', () => { + middleware(Actions.updateInvoice(currentInvoice)).then(() => + updateDoc('invoices', currentInvoice).then(data => { + // Call next after the promised is returned + expect(next.mock.calls.length).toBe(1); + expect(next).toHaveBeenCalledWith({ + type: ACTION_TYPES.INVOICE_UPDATE, + payload: mockData.invoicesRecords, + }); + // Dispatch success notification + expect(dispatch.mock.calls.length).toBe(1); + expect(dispatch).toHaveBeenCalledWith({ + type: ACTION_TYPES.UI_NOTIFICATION_NEW, + payload: { + type: 'success', + message: 'Invoice Updated Successfully', + }, + }); + }) + ); + }); + + it('should handle syntax error correctly', () => { + middleware(Actions.updateInvoice(currentInvoice)).then(() => { + const dbError = new Error('No database found!'); + const docError = new Error('No doc found!'); + expect(updateDoc()).rejects.toEqual(dbError); + expect(updateDoc('invoices')).rejects.toEqual(docError); + }); + }); + + it('should handle unkown error correctly', () => { + const expectedError = new Error('Something Broken!'); + updateDoc.mockImplementationOnce(() => Promise.reject(expectedError)); + middleware(Actions.updateInvoice(currentInvoice)).then(() => { + expect(next).toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith({ + type: ACTION_TYPES.UI_NOTIFICATION_NEW, + payload: { + type: 'warning', + message: expectedError.message, + }, + }); + }); + }); + }) + + describe('should handle INVOICE_DELETE action', () => { it('should remove record from DB correctly', () => { const invoiceID = 'jon-invoice'; diff --git a/app/reducers/FormReducer.jsx b/app/reducers/FormReducer.jsx index 0015cb6e..c41d066a 100644 --- a/app/reducers/FormReducer.jsx +++ b/app/reducers/FormReducer.jsx @@ -25,7 +25,7 @@ const initialState = { settings: { open: false, editMode: { - active: false + active: false, }, required_fields: invoiceSettings.required_fields, }, @@ -123,14 +123,20 @@ const FormReducer = handleActions( }), rows, currency, - dueDate: dueDate !== undefined ? Object.assign({}, state.dueDate, { - selectedDate: dueDate - }) : state.dueDate, + dueDate: + dueDate !== undefined + ? Object.assign({}, state.dueDate, { + selectedDate: dueDate, + }) + : state.dueDate, discount: discount !== undefined ? discount : state.discount, tax: tax !== undefined ? tax : state.tax, - note: note !== undefined ? Object.assign({}, state.note, { - content: note - }) : state.note, + note: + note !== undefined + ? Object.assign({}, state.note, { + content: note, + }) + : state.note, // Update settings settings: Object.assign({}, state.settings, { editMode: { @@ -143,7 +149,7 @@ const FormReducer = handleActions( dueDate: dueDate !== undefined, discount: discount !== undefined, note: note !== undefined, - }) + }), }), }); }, @@ -169,7 +175,7 @@ const FormReducer = handleActions( settings: Object.assign({}, state.settings, { open: false, editMode: { - active: false + active: false, }, required_fields: state.savedSettings.required_fields, }), diff --git a/app/reducers/__tests__/FormReducer.spec.js b/app/reducers/__tests__/FormReducer.spec.js index 9021576a..9bea5a22 100644 --- a/app/reducers/__tests__/FormReducer.spec.js +++ b/app/reducers/__tests__/FormReducer.spec.js @@ -311,6 +311,211 @@ describe('Form Reducer should handle update', () => { }); }); +describe('Form Reducer should handle Invoice Edit', () => { + let currentState, invoiceData, newState; + beforeEach(() => { + currentState = { + recipient: { + newRecipient: true, + select: {}, + new: {}, + }, + rows: [], + dueDate: {}, + discount: {}, + note: {}, + currency: { + code: 'USD', + decimal_digits: 2, + name: 'US Dollar', + name_plural: 'US dollars', + rounding: 0, + symbol: '$', + symbol_native: '$', + }, + tax: { + amount: 10, + method: 'default', + tin: '123-456-789', + }, + settings: { + open: false, + editMode: { + active: false, + }, + required_fields: { + dueDate: false, + currency: false, + discount: false, + tax: false, + note: false, + }, + }, + savedSettings: { + currency: 'VND', + }, + }; + invoiceData = { + recipient: { + fullname: 'Jon Snow', + company: 'HBO', + email: 'jon@snow.got', + phone: '000000000', + }, + rows: [ + { + id: '16bf1a07-71e6-4be4-8a18-d89a715bd191', + description: 'iPhone X', + price: 999, + quantity: 1, + subtotal: 999 + }, + ], + currency: { + code: 'USD', + decimal_digits: 2, + name: 'US Dollar', + name_plural: 'US dollars', + rounding: 0, + symbol: '$', + symbol_native: '$', + }, + tax: { + amount: 5, + method: 'reverse-charge', + tin: '555-444-333', + }, + dueDate: { + date: 1, + hours: 12, + milliseconds: 0, + minutes: 0, + months: 1, + seconds: 0, + years: 2018, + }, + discount: { + amount: 5, + type: 'percentage' + }, + note: 'Thank you!', + }; + newState = FormReducer(currentState, { + type: ACTION_TYPES.INVOICE_EDIT, + payload: invoiceData, + }); + }); + + it('change editMode to true and add editData', () => { + expect(newState.settings.editMode.active).toEqual(true); + expect(newState.settings.editMode.data).toEqual(invoiceData); + }); + + it('should populate field data', () => { + // Recipient + expect(newState.recipient.newRecipient).toEqual(false); + expect(newState.recipient.select).toEqual(invoiceData.recipient); + // Rows + expect(newState.rows.length).toEqual(1); + expect(newState.rows).toEqual(invoiceData.rows); + // Tax + expect(newState.tax).toEqual(invoiceData.tax); + // Currency + expect(newState.currency).toEqual(invoiceData.currency); + // Note + expect(newState.note.content).toEqual(invoiceData.note); + // Discount + expect(newState.discount).toEqual(invoiceData.discount); + // DueDate + expect(newState.dueDate.selectedDate).toEqual(invoiceData.dueDate); + }); + + it('toggle optional field if necessary', () => { + const { required_fields } = newState.settings; + expect(required_fields.tax).toEqual(invoiceData.tax !== undefined); + expect(required_fields.dueDate).toEqual(invoiceData.dueDate !== undefined); + expect(required_fields.discount).toEqual(invoiceData.discount !== undefined); + expect(required_fields.note).toEqual(invoiceData.note !== undefined); + }); + + it('should only show currency field if it is different than default', () => { + const currentCurrencyCode = newState.currency.code; + const savedCurrencyCode = newState.savedSettings.currency; + expect(newState.settings.required_fields.currency).toEqual( + currentCurrencyCode !== savedCurrencyCode + ); + }); +}); + +describe('Form Reducer should handle update Settings', () => { + let currentState, newState; + beforeEach(() => { + currentState = { + recipient: { + newRecipient: true, + select: {}, + new: {}, + }, + rows: [], + savedSettings: { + tax: { + amount: 10, + method: 'default', + tin: '123-456-789', + }, + currency: 'USD', + required_fields: { + dueDate: false, + currency: false, + discount: false, + tax: false, + note: false, + } + }, + }; + newState = FormReducer(currentState, { + type: ACTION_TYPES.SAVED_FORM_SETTING_UPDATE, + payload: { + tax: { + amount: 5, + method: 'reverse', + tin: '111-111-111', + }, + currency: 'VND', + required_fields: { + dueDate: true, + currency: true, + discount: true, + tax: true, + note: true, + } + }, + }); + }); + + it('should save default tax value', () => { + expect(newState.savedSettings.tax).toEqual({ + amount: 5, + method: 'reverse', + tin: '111-111-111', + }); + }); + + it('should save default currency value', () => { + expect(newState.savedSettings.currency).toEqual('VND'); + }); + + it('should save default visibility value', () => { + expect(newState.savedSettings.required_fields).toEqual({ + dueDate: true, + currency: true, + discount: true, + tax: true, + note: true, + }); + }); +}); + // Test Selectors const state = { form: {