Skip to content

Commit

Permalink
Allow user to duplicate an invoice. (hql287#272)
Browse files Browse the repository at this point in the history
* Added duplicateInvoice action

* Added & updated tests
  • Loading branch information
hql287 authored Mar 17, 2018
1 parent 8c91e2b commit d50c3ae
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 38 deletions.
12 changes: 12 additions & 0 deletions app/actions/__tests__/invoices.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ it('saveInvoice should create INVOICE_SAVE action', () => {
});
});

it('duplicateInvoice should create INVOICE_DUPLICATE action', () => {
const invoiceData = {
_id: 'jon_snow',
fulname: 'Jon Snow',
email: '[email protected]',
};
expect(actions.duplicateInvoice(invoiceData)).toEqual({
type: ACTION_TYPES.INVOICE_DUPLICATE,
payload: invoiceData,
});
});

it('newInvoiceFromContact should create INVOICE_NEW_FROM_CONTACT action', () => {
const contactData = {
_id: 'jon_snow',
Expand Down
5 changes: 5 additions & 0 deletions app/actions/invoices.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export const saveInvoice = createAction(
invoiceData => invoiceData
);

export const duplicateInvoice = createAction(
ACTION_TYPES.INVOICE_DUPLICATE,
invoiceData => invoiceData
);

export const newInvoiceFromContact = createAction(
ACTION_TYPES.INVOICE_NEW_FROM_CONTACT,
contact => contact
Expand Down
81 changes: 57 additions & 24 deletions app/components/invoices/Invoice.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,6 @@ const Header = styled.div`
justify-content: space-between;
align-items: center;
padding: 25px 30px;
i.ion-trash-a {
font-size: 24px;
line-height: 24px;
color: #c4c8cc;
}
i.ion-checkmark {
font-size: 16px;
line-height: 16px;
}
i.ion-loop {
font-size: 18px;
line-height: 18px;
}
i.ion-backspace {
font-size: 18px;
line-height: 18px;
}
i.ion-arrow-return-left {
font-size: 18px;
line-height: 18px;
}
`;

const StatusBar = styled.div`
Expand Down Expand Up @@ -81,6 +60,48 @@ const Status = styled.div`
margin-right: 5px;
}
}
i.ion-checkmark {
font-size: 16px;
line-height: 16px;
}
i.ion-loop {
font-size: 18px;
line-height: 18px;
}
i.ion-backspace {
font-size: 18px;
line-height: 18px;
}
i.ion-arrow-return-left {
font-size: 18px;
line-height: 18px;
}
`;

const ButtonsGroup = styled.div`
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
i {
margin-left: 10px;
color: #B4B7BA;
}
i.ion-trash-a {
font-size: 24px;
line-height: 24px;
&:hover {
color: #EC476E;
}
}
i.ion-ios-copy {
font-size: 24px;
line-height: 24px;
&:hover {
color: #469FE5;
}
}
}
`;

// Invoice Body
Expand Down Expand Up @@ -148,6 +169,7 @@ class Invoice extends PureComponent {
this.viewInvoice = this.viewInvoice.bind(this);
this.editInvoice = this.editInvoice.bind(this);
this.deleteInvoice = this.deleteInvoice.bind(this);
this.duplicateInvoice = this.duplicateInvoice.bind(this);
this.displayStatus = this.displayStatus.bind(this);
}

Expand All @@ -156,6 +178,11 @@ class Invoice extends PureComponent {
deleteInvoice(invoice._id);
}

duplicateInvoice() {
const { invoice, duplicateInvoice } = this.props;
duplicateInvoice(invoice);
}

editInvoice() {
const { invoice, editInvoice } = this.props;
editInvoice(invoice);
Expand Down Expand Up @@ -249,9 +276,14 @@ class Invoice extends PureComponent {
<StatusBar status={status} />
<Header>
<Status status={status}>{this.displayStatus()}</Status>
<Button link onClick={this.deleteInvoice}>
<i className="ion-trash-a" />
</Button>
<ButtonsGroup>
<Button link onClick={this.duplicateInvoice}>
<i className="ion-ios-copy" />
</Button>
<Button link onClick={this.deleteInvoice}>
<i className="ion-trash-a" />
</Button>
</ButtonsGroup>
</Header>
<Body>
<Row>
Expand Down Expand Up @@ -324,6 +356,7 @@ class Invoice extends PureComponent {
Invoice.propTypes = {
dateFormat: PropTypes.string.isRequired,
deleteInvoice: PropTypes.func.isRequired,
duplicateInvoice: PropTypes.func.isRequired,
editInvoice: PropTypes.func.isRequired,
invoice: PropTypes.object.isRequired,
setInvoiceStatus: PropTypes.func.isRequired,
Expand Down
32 changes: 27 additions & 5 deletions app/components/invoices/__tests__/Invoice.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import Invoice from '../Invoice';
import Button from '../../shared/Button';
import SplitButton from '../../shared/SplitButton';


// Mocks
const invoice = {
_id: '37b2804e-bfc1-4289-b1d7-226c5652ac91',
Expand All @@ -35,6 +34,7 @@ const invoice = {

const editInvoice = jest.fn();
const deleteInvoice = jest.fn();
const duplicateInvoice = jest.fn();
const setInvoiceStatus = jest.fn();
const dateFormat = 'MM/DD/YY';
const currencyPlacement = 'before';
Expand Down Expand Up @@ -67,6 +67,7 @@ describe('Renders correctly to the DOM', () => {
invoice={invoice}
editInvoice={editInvoice}
deleteInvoice={deleteInvoice}
duplicateInvoice={duplicateInvoice}
setInvoiceStatus={setInvoiceStatus}
dateFormat={dateFormat}
currencyPlacement={currencyPlacement}
Expand All @@ -81,6 +82,7 @@ describe('Renders correctly to the DOM', () => {
invoice={invoice}
editInvoice={editInvoice}
deleteInvoice={deleteInvoice}
duplicateInvoice={duplicateInvoice}
setInvoiceStatus={setInvoiceStatus}
dateFormat={dateFormat}
currencyPlacement={currencyPlacement}
Expand Down Expand Up @@ -121,6 +123,7 @@ describe('Renders correctly to the DOM', () => {
invoice={paidInvoice}
editInvoice={editInvoice}
deleteInvoice={deleteInvoice}
duplicateInvoice={duplicateInvoice}
setInvoiceStatus={setInvoiceStatus}
dateFormat={dateFormat}
currencyPlacement={currencyPlacement}
Expand All @@ -145,6 +148,7 @@ describe('Renders correctly to the DOM', () => {
invoice={cancelledInvoice}
editInvoice={editInvoice}
deleteInvoice={deleteInvoice}
duplicateInvoice={duplicateInvoice}
setInvoiceStatus={setInvoiceStatus}
dateFormat={dateFormat}
currencyPlacement={currencyPlacement}
Expand All @@ -167,6 +171,7 @@ describe('Renders correctly to the DOM', () => {
invoice={refundedInvoice}
editInvoice={editInvoice}
deleteInvoice={deleteInvoice}
duplicateInvoice={duplicateInvoice}
setInvoiceStatus={setInvoiceStatus}
dateFormat={dateFormat}
currencyPlacement={currencyPlacement}
Expand All @@ -181,8 +186,8 @@ describe('Renders correctly to the DOM', () => {
).toEqual('Refunded');
});

it('render delete button in the header', () => {
expect(wrapper.find('Invoice__Header').find(Button)).toHaveLength(1);
it('render delete and duplicate button in the header', () => {
expect(wrapper.find('Invoice__Header').find(Button)).toHaveLength(2);
});

it('render a split button and 2 buttons in the footer', () => {
Expand All @@ -198,6 +203,7 @@ describe('Renders correctly to the DOM', () => {
invoice={invoice}
editInvoice={editInvoice}
deleteInvoice={deleteInvoice}
duplicateInvoice={duplicateInvoice}
setInvoiceStatus={setInvoiceStatus}
dateFormat={dateFormat}
currencyPlacement={currencyPlacement}
Expand All @@ -209,16 +215,25 @@ describe('Renders correctly to the DOM', () => {
});

describe('handle clicks correctly', () => {
let wrapper, editBtn, viewBtn, deleteBtn, spyViewAction, spyEditAction;
let wrapper,
editBtn,
viewBtn,
duplicateBtn,
deleteBtn,
spyViewAction,
spyEditAction,
spyDuplicateAction;
beforeEach(() => {
spyViewAction = jest.spyOn(Invoice.prototype, 'viewInvoice');
spyEditAction = jest.spyOn(Invoice.prototype, 'editInvoice');
spyDuplicateAction = jest.spyOn(Invoice.prototype, 'duplicateInvoice');
wrapper = shallow(
<Invoice
t={t}
invoice={invoice}
editInvoice={editInvoice}
deleteInvoice={deleteInvoice}
duplicateInvoice={duplicateInvoice}
setInvoiceStatus={setInvoiceStatus}
dateFormat={dateFormat}
currencyPlacement={currencyPlacement}
Expand All @@ -232,7 +247,8 @@ describe('handle clicks correctly', () => {
.find('Invoice__Footer')
.find(Button)
.first();
deleteBtn = wrapper.find(Button).first();
deleteBtn = wrapper.find(Button).at(1);
duplicateBtn = wrapper.find(Button).first();
});

it('handle edit action correctly', () => {
Expand All @@ -247,6 +263,12 @@ describe('handle clicks correctly', () => {
expect(deleteInvoice).toHaveBeenCalledWith(invoice._id);
});

it('handle duplicate action correctly', () => {
duplicateBtn.simulate('click');
expect(duplicateInvoice).toHaveBeenCalled();
expect(duplicateInvoice).toHaveBeenCalledWith(invoice);
});

it('handle view action correctly', () => {
viewBtn.simulate('click');
expect(spyViewAction).toHaveBeenCalled();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ exports[`Renders correctly to the DOM matches snapshot 1`] = `
className="Invoice__StatusBar-jBWtSa joKiG"
/>
<div
className="Invoice__Header-cGSpHV wcQzb"
className="Invoice__Header-cGSpHV gmDFcR"
>
<div
className="Invoice__Status-fxIELu cyYgut"
className="Invoice__Status-fxIELu bbALRd"
>
<span>
<i
Expand All @@ -23,14 +23,26 @@ exports[`Renders correctly to the DOM matches snapshot 1`] = `
Pending
</span>
</div>
<button
className="Button__ButtonLinkStyle-caVwCA imXwRc"
onClick={[Function]}
<div
className="Invoice__ButtonsGroup-PwNbx emusdh"
>
<i
className="ion-trash-a"
/>
</button>
<button
className="Button__ButtonLinkStyle-caVwCA imXwRc"
onClick={[Function]}
>
<i
className="ion-ios-copy"
/>
</button>
<button
className="Button__ButtonLinkStyle-caVwCA imXwRc"
onClick={[Function]}
>
<i
className="ion-trash-a"
/>
</button>
</div>
</div>
<div
className="Invoice__Body-eBItTh igYqzw"
Expand Down
1 change: 1 addition & 0 deletions app/constants/actions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const INVOICE_GET_ALL = 'INVOICE_GET_ALL';
export const INVOICE_NEW_FROM_CONTACT = 'INVOICE_NEW_FROM_CONTACT';
export const INVOICE_SET_STATUS = 'INVOICE_SET_STATUS';
export const INVOICE_CONFIGS_SAVE = 'INVOICE_CONFIGS_SAVE';
export const INVOICE_DUPLICATE = 'INVOICE_DUPLICATE';

// CONTACTS
// ===========================================================
Expand Down
7 changes: 7 additions & 0 deletions app/containers/Invoices.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class Invoices extends PureComponent {
super(props);
this.editInvoice = this.editInvoice.bind(this);
this.deleteInvoice = this.deleteInvoice.bind(this);
this.duplicateInvoice = this.duplicateInvoice.bind(this);
this.setInvoiceStatus = this.setInvoiceStatus.bind(this);
}

Expand Down Expand Up @@ -83,6 +84,11 @@ class Invoices extends PureComponent {
dispatch(Actions.editInvoice(invoice));
}

duplicateInvoice(invoice) {
const { dispatch } = this.props;
dispatch(Actions.duplicateInvoice(invoice));
}

// Render
render() {
const { dateFormat, invoices, t } = this.props;
Expand All @@ -91,6 +97,7 @@ class Invoices extends PureComponent {
key={invoice._id}
dateFormat={dateFormat}
deleteInvoice={this.deleteInvoice}
duplicateInvoice={this.duplicateInvoice}
editInvoice={this.editInvoice}
setInvoiceStatus={this.setInvoiceStatus}
index={index}
Expand Down
12 changes: 12 additions & 0 deletions app/middlewares/InvoicesMW.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,18 @@ const InvoicesMW = ({ dispatch, getState }) => next => action => {
});
}

case ACTION_TYPES.INVOICE_DUPLICATE: {
const duplicateInvoice = Object.assign({}, action.payload, {
created_at: Date.now(),
_id: uuidv4(),
_rev: null,
})
return dispatch({
type: ACTION_TYPES.INVOICE_SAVE,
payload: duplicateInvoice,
});
}

case ACTION_TYPES.INVOICE_UPDATE: {
return updateDoc('invoices', action.payload)
.then(docs => {
Expand Down
Loading

0 comments on commit d50c3ae

Please sign in to comment.