Skip to content

Commit

Permalink
Added filtering invoices feature (hql287#275)
Browse files Browse the repository at this point in the history
* Added filter to Invoices page

* Added ButtonsGroup component & refactored Buttons CSS

* Updated test snapshots

* Added tests

* Updated Snapshot

* Refactored Test

* Added translation for filter buttons label

* Updated snapshot
  • Loading branch information
hql287 authored Mar 21, 2018
1 parent 8132d6c commit 0b27f2c
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ exports[`Note component matches snapshot 1`] = `
className="itemsListActions"
>
<button
className="ItemsList__ItemsListActionsBtn-fnweJK bouIBy Button__ButtonStyle-coFSWz hJbnhZ"
className="ItemsList__ItemsListActionsBtn-fnweJK bouIBy Button__ButtonStyle-coFSWz drmiqa"
onClick={
[MockFunction] {
"calls": Array [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,13 @@ exports[`Renders correctly to the DOM matches snapshot 1`] = `
</div>
</div>
<button
className="Button__ButtonStyle-coFSWz llSIFU"
className="Button__ButtonStyle-coFSWz coAhhp"
onClick={[Function]}
>
Pending
</button>
<button
className="Button__ButtonStyle-coFSWz llSIFU"
className="Button__ButtonStyle-coFSWz coAhhp"
onClick={[Function]}
>
Pending
Expand Down
52 changes: 38 additions & 14 deletions app/components/shared/Button.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,34 @@ const ButtonStyle = styled.button`
font-size: 12px;
text-decoration: none;
background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.1);
border: 1px solid #e0e1e1;
text-transform: uppercase;
letter-spacing: 1px;
${props => props.block && `width: 100%;`} ${props =>
props.primary &&
`
// Block Level Button
${props => props.block && `width: 100%;`}
// Color
${props => props.primary && `
background: #469fe5;
color: white;
`} ${props =>
props.success &&
`
`}
${props => props.success && `
background: #6bbb69;
color: white;
`} ${props =>
props.danger &&
`
`}
${props => props.danger && `
background: #EC476E;
color: white;
`} &:hover {
`}
// Active state
${props => props.active && `
background: #F2F3F4;
color: #4F555C;
`}
// Hover
&:hover {
cursor: pointer;
// color: white;
text-decoration: none;
// color: white;
}
`;

Expand All @@ -50,12 +56,26 @@ const ButtonLinkStyle = styled.button`
padding: 0;
margin: 0;
${props => props.primary && `color: #469fe5;`} ${props =>
props.success && `color: #6bbb69;`} ${props =>
props.danger && `color: #EC476E;`} &:hover {
props.success && `color: #6bbb69;`} ${props =>
props.danger && `color: #EC476E;`} &:hover {
cursor: pointer;
}
`;

const ButtonsGroupStyle = styled.div`
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
> button {
margin: 0!important;
border-radius: 0;
&:first-child { border-radius: 4px 0 0 4px; }
&:last-child { border-radius: 0 4px 4px 0; }
&:not(:first-child) { border-left: 0; }
}
`;

function Button(props) {
return props.link ? (
<ButtonLinkStyle {...props}>{props.children}</ButtonLinkStyle>
Expand All @@ -79,4 +99,8 @@ Button.defaultProps = {
danger: false,
};

export const ButtonsGroup = props => (
<ButtonsGroupStyle>{props.children}</ButtonsGroupStyle>
);

export default Button;
9 changes: 6 additions & 3 deletions app/components/shared/Layout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@ const PageHeaderTitleStyle = styled.p`
`;

const PageHeaderActionsStyle = styled.div`
button {
margin-left: 10px;
}
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
button { margin-left: 10px; }
i { margin-right: 10px; }
`;

const PageContentStyle = styled.div`
Expand Down
37 changes: 34 additions & 3 deletions app/containers/Invoices.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,25 @@ import { getDateFormat } from '../reducers/SettingsReducer';
// Components
import Invoice from '../components/invoices/Invoice';
import Message from '../components/shared/Message';
import Button, { ButtonsGroup } from '../components/shared/Button';
import _withFadeInAnimation from '../components/shared/hoc/_withFadeInAnimation';
import {
PageWrapper,
PageHeader,
PageHeaderTitle,
PageHeaderActions,
PageContent,
} from '../components/shared/Layout';

class Invoices extends PureComponent {
export class Invoices extends PureComponent {
constructor(props) {
super(props);
this.state = { filter: null };
this.editInvoice = this.editInvoice.bind(this);
this.deleteInvoice = this.deleteInvoice.bind(this);
this.duplicateInvoice = this.duplicateInvoice.bind(this);
this.setInvoiceStatus = this.setInvoiceStatus.bind(this);
this.setFilter = this.setFilter.bind(this);
}

// Load Invoices & add event listeners
Expand Down Expand Up @@ -89,10 +93,18 @@ class Invoices extends PureComponent {
dispatch(Actions.duplicateInvoice(invoice));
}

setFilter(event) {
const currentFilter = this.state.filter;
const newFilter = event.target.dataset.filter;
this.setState({ filter: currentFilter === newFilter ? null : newFilter });
}

// Render
render() {
const { dateFormat, invoices, t } = this.props;
const invoicesComponent = invoices.map((invoice, index) => (
const { filter } = this.state;
const filteredInvoices = filter ? invoices.filter(invoice => invoice.status === filter) : invoices
const invoicesComponent = filteredInvoices.map((invoice, index) => (
<Invoice
key={invoice._id}
dateFormat={dateFormat}
Expand All @@ -105,16 +117,35 @@ class Invoices extends PureComponent {
t={t}
/>
));
// Filter Buttons
const statuses = ['paid', 'pending', 'refunded', 'cancelled'];
const filterButtons = statuses.map(status => (
<Button
key={`${status}-button`}
active={filter === status}
data-filter={status}
onClick={this.setFilter}
>
{ t(`invoices:status:${status}`) }
</Button>
));

return (
<PageWrapper>
<PageHeader>
<PageHeaderTitle>{t('invoices:header:name')}</PageHeaderTitle>
<PageHeaderActions>
<i className="ion-funnel" />
<ButtonsGroup>{ filterButtons }</ButtonsGroup>
</PageHeaderActions>
</PageHeader>
<PageContent bare>
{invoices.length === 0 ? (
<Message info text={t('messages:noInvoice')} />
) : (
<div className="row">{invoicesComponent}</div>
<div className="row">
{invoicesComponent}
</div>
)}
</PageContent>
</PageWrapper>
Expand Down
127 changes: 125 additions & 2 deletions app/containers/__tests__/Invoices.spec.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,125 @@
import Invoices from '../Invoices';
it('placeholder');
import React from 'react';
import { render, shallow, mount } from 'enzyme';
import renderer from 'react-test-renderer';

// Component
import {
PageWrapper,
PageHeader,
PageHeaderTitle,
PageHeaderActions,
PageContent,
} from '../../components/shared/Layout';
import { Invoices } from '../Invoices';
import Button, { ButtonsGroup } from '../../components/shared/Button';

// Mocks
const dispatch = jest.fn();
const t = jest.fn();
const invoices = [
{
_id: 'first-invoice',
status: 'pending',
},
{
_id: 'second-invoice',
status: 'refunded',
},
{
_id: 'third-invoice',
status: 'paid',
},
{
_id: 'fourth-invoice',
status: 'cancelled',
},
];
jest.mock('../../components/invoices/Invoice', () => () => (
<div className="invoice" />
));

jest.mock('../../components/shared/Message', () => () => (
<div className="message" />
));

describe('render component correctly', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(<Invoices t={t} invoices={invoices} dispatch={dispatch} />);
});

it('receives correct props', () => {
const mountWrapper = mount(
<Invoices t={t} invoices={invoices} dispatch={dispatch} />
);
expect(mountWrapper.prop('t')).toEqual(t);
expect(mountWrapper.prop('dispatch')).toEqual(dispatch);
expect(mountWrapper.prop('invoices')).toEqual(invoices);
});

it('render correct number of invoices', () => {
expect(wrapper.find('.invoice')).toHaveLength(invoices.length);
});

it('render empty message when there is no invoice in the DB', () => {
const newWrapper = mount(
<Invoices t={t} invoices={[]} dispatch={dispatch} />
);
expect(newWrapper.find('.message')).toHaveLength(1);
});

it('render a correct page layout', () => {
expect(wrapper.find(PageWrapper)).toHaveLength(1);
expect(wrapper.find(PageHeader)).toHaveLength(1);
expect(wrapper.find(PageHeaderTitle)).toHaveLength(1);
expect(wrapper.find(PageHeaderActions)).toHaveLength(1);
expect(wrapper.find(PageContent)).toHaveLength(1);
expect(wrapper.find(PageContent)).toHaveLength(1);
});

it('renders filter with four options', () => {
const HeaderActions = wrapper.find(PageHeaderActions).first();
expect(HeaderActions.find(Button)).toHaveLength(4);
});

it('matches snapshot', () => {
const tree = renderer
.create(<Invoices t={t} dispatch={dispatch} invoices={invoices} />)
.toJSON();
expect(tree).toMatchSnapshot();
});
});

describe('handle actions correcrtly', () => {
let wrapper, spySetFilter, actions, filterBtn;
beforeEach(() => {
spySetFilter = jest.spyOn(Invoices.prototype, 'setFilter');
wrapper = mount(
<Invoices t={t} invoices={invoices} dispatch={dispatch} />
);
actions = wrapper.find(PageHeaderActions).first();
filterBtn = actions.find(Button).last();
filterBtn.simulate('click', {
target: {
dataset: {
filter: 'paid',
},
},
});
});

it('set correct filter', () => {
expect(spySetFilter).toHaveBeenCalled();
const filter = wrapper.state().filter;
expect(filter).toEqual('paid');
expect(filter).not.toEqual('pending');
});

it('display only invoices with matched status', () => {
const filter = wrapper.state().filter;
const filteredInvoices = invoices.filter(
invoice => invoice.status === filter
);
expect(wrapper.find('.invoice')).toHaveLength(filteredInvoices.length);
});
});
Loading

0 comments on commit 0b27f2c

Please sign in to comment.