From bd72dd867b321fb31a2c6a27a5b7d8c593a1d9d5 Mon Sep 17 00:00:00 2001
From: Kizito Akhilome
Date: Tue, 29 Jan 2019 21:16:04 +0100
Subject: [PATCH] feat(cancel-order): implement cancel order functionality
- add button to order card for user to be able to cancel an order
- implement order cancellation logic using redux
- adequately test out all implementations
- improve UX by adding cart count and moving toasts to bottom
[Finishes #163354061]
---
src/actions/index.js | 14 ++++++
src/actions/types/index.js | 2 +
src/components/Nav.jsx | 11 ++++-
src/components/OrderCard.jsx | 12 ++++-
src/components/OrderHistory.jsx | 33 +++++++++-----
src/index.jsx | 2 +-
src/reducers/ordersReducer.js | 6 +++
src/tests/actions/orders.test.js | 40 ++++++++++++++++-
src/tests/components/OrderHistory.test.jsx | 10 +++++
.../__snapshots__/App.test.jsx.snap | 2 +
src/tests/reducers/orders.test.js | 44 +++++++++++++++++--
11 files changed, 156 insertions(+), 20 deletions(-)
diff --git a/src/actions/index.js b/src/actions/index.js
index cb7fef9..69652b0 100644
--- a/src/actions/index.js
+++ b/src/actions/index.js
@@ -140,6 +140,20 @@ export const getUserOrderHistory = () => async (dispatch) => {
}
};
+export const cancelOrder = orderId => async (dispatch) => {
+ dispatch(startFetching());
+ try {
+ await axios.delete(`/orders/${orderId}`);
+ dispatch({ type: types.CANCEL_ORDER, payload: { orderId } });
+ dispatch(stopFetching());
+ return toast.success('Order cancelled successfully 🎉');
+ } catch (error) {
+ dispatch({ type: types.CANCEL_ORDER_FAIL });
+ dispatch(stopFetching(false, errorResponse(error)));
+ return toast.error(errorResponse(error));
+ }
+};
+
export const logout = () => (dispatch) => {
dispatch(emptyCart());
removeToken();
diff --git a/src/actions/types/index.js b/src/actions/types/index.js
index 0fa6824..0db8a14 100644
--- a/src/actions/types/index.js
+++ b/src/actions/types/index.js
@@ -16,4 +16,6 @@ export default {
EMPTY_CART: 'EMPTY_CART',
GET_ORDER_HISTORY: 'GET_ORDER_HISTORY',
GET_ORDER_HISTORY_FAIL: 'GET_ORDER_HISTORY_FAIL',
+ CANCEL_ORDER: 'CANCEL_ORDER',
+ CANCEL_ORDER_FAIL: 'CANCEL_ORDER_FAIL',
};
diff --git a/src/components/Nav.jsx b/src/components/Nav.jsx
index cd2912c..800957a 100644
--- a/src/components/Nav.jsx
+++ b/src/components/Nav.jsx
@@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import LogoutLink from './Logout';
-export const Nav = ({ isLoggedIn, role }) => {
+export const Nav = ({ isLoggedIn, role, cartItemsCount }) => {
const navContent = () => {
if (isLoggedIn && role === 'customer') {
return (
@@ -13,7 +13,11 @@ export const Nav = ({ isLoggedIn, role }) => {
Menu
- Cart
+
+Cart [
+ {cartItemsCount}
+]
+
Order History
@@ -69,15 +73,18 @@ export const Nav = ({ isLoggedIn, role }) => {
Nav.propTypes = {
isLoggedIn: PropTypes.bool,
role: PropTypes.string.isRequired,
+ cartItemsCount: PropTypes.number,
};
Nav.defaultProps = {
isLoggedIn: false,
+ cartItemsCount: 0,
};
const mapStateToProps = state => ({
isLoggedIn: state.user.isLoggedIn,
role: state.user.role,
+ cartItemsCount: Object.keys(state.cart).length,
});
export default connect(mapStateToProps)(Nav);
diff --git a/src/components/OrderCard.jsx b/src/components/OrderCard.jsx
index dcd7bc4..bcb364d 100644
--- a/src/components/OrderCard.jsx
+++ b/src/components/OrderCard.jsx
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
const OrderCard = ({
- foodItems, orderPrice, date, orderStatus,
+ foodItems, orderPrice, date, orderStatus, cancelOrderCallback,
}) => {
const formattedFoodNames = foodItems.map(foodName => (
{foodName}
@@ -48,7 +48,14 @@ const OrderCard = ({
.join('/')}
-
+
);
};
@@ -58,6 +65,7 @@ OrderCard.propTypes = {
orderPrice: PropTypes.number.isRequired,
date: PropTypes.string.isRequired,
orderStatus: PropTypes.string.isRequired,
+ cancelOrderCallback: PropTypes.func.isRequired,
};
export default OrderCard;
diff --git a/src/components/OrderHistory.jsx b/src/components/OrderHistory.jsx
index 8586952..f6758a6 100644
--- a/src/components/OrderHistory.jsx
+++ b/src/components/OrderHistory.jsx
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import OrderCard from './OrderCard';
-import { getUserOrderHistory } from '../actions';
+import { getUserOrderHistory, cancelOrder } from '../actions';
export class OrderHistory extends Component {
async componentDidMount() {
@@ -10,21 +10,31 @@ export class OrderHistory extends Component {
await getOrderHistory();
}
+ onCancelOrder = async (orderId) => {
+ const { cancelOrder: deleteOrder } = this.props;
+ await deleteOrder(Number(orderId));
+ };
+
render() {
const { orders } = this.props;
return (
- {orders.map(order => (
-
- ))}
+ {orders.length ? (
+ orders.map(order => (
+ this.onCancelOrder(order.id)}
+ />
+ ))
+ ) : (
+ No Orders Yet!
+ )}
);
@@ -34,6 +44,7 @@ export class OrderHistory extends Component {
OrderHistory.propTypes = {
getUserOrderHistory: PropTypes.func.isRequired,
orders: PropTypes.instanceOf(Array).isRequired,
+ cancelOrder: PropTypes.func.isRequired,
};
const mapStateToProps = state => ({
@@ -42,5 +53,5 @@ const mapStateToProps = state => ({
export default connect(
mapStateToProps,
- { getUserOrderHistory },
+ { getUserOrderHistory, cancelOrder },
)(OrderHistory);
diff --git a/src/index.jsx b/src/index.jsx
index 6d61893..e83ca88 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -15,7 +15,7 @@ const store = createStore(reducers, composeEnhancers(applyMiddleware(thunk)));
const jsx = (
-
+
);
diff --git a/src/reducers/ordersReducer.js b/src/reducers/ordersReducer.js
index 794586f..8bc5d24 100644
--- a/src/reducers/ordersReducer.js
+++ b/src/reducers/ordersReducer.js
@@ -8,7 +8,13 @@ export default (state = intialState, action) => {
switch (action.type) {
case types.GET_ORDER_HISTORY:
return { ...intialState, history: action.payload.orders };
+ case types.CANCEL_ORDER:
+ return {
+ ...state,
+ history: state.history.filter(order => order.id !== action.payload.orderId),
+ };
case types.GET_ORDER_HISTORY_FAIL:
+ case types.CANCEL_ORDER_FAIL:
default:
return state;
}
diff --git a/src/tests/actions/orders.test.js b/src/tests/actions/orders.test.js
index 2fc30c7..055f716 100644
--- a/src/tests/actions/orders.test.js
+++ b/src/tests/actions/orders.test.js
@@ -1,7 +1,9 @@
-import { getUserOrderHistory } from '../../actions';
+import { getUserOrderHistory, cancelOrder } from '../../actions';
import jwt from '../../utils/jwt';
import axios from '../../services/axios';
+afterAll(() => jest.restoreAllMocks());
+
describe('getUserOrderHistory', () => {
jest.spyOn(jwt, 'decode').mockImplementation(() => ({ userId: 1 }));
const dispatch = jest.fn();
@@ -35,3 +37,39 @@ describe('getUserOrderHistory', () => {
});
});
});
+
+describe('cancelOrder()', () => {
+ const dispatch = jest.fn();
+ const errResponse = { response: { data: { message: 'failed to cancel order' } } };
+ afterEach(() => jest.resetAllMocks());
+
+ it('should dispatch correct action to cancel an order', async () => {
+ jest.spyOn(axios, 'delete').mockImplementation(() => Promise.resolve());
+ await cancelOrder(7)(dispatch);
+
+ expect(dispatch).toHaveBeenCalledTimes(3);
+ expect(dispatch.mock.calls[1][0]).toEqual({
+ type: 'CANCEL_ORDER',
+ payload: { orderId: 7 },
+ });
+ expect(dispatch).toHaveBeenLastCalledWith({
+ type: 'STOP_FETCHING',
+ payload: { error: false, message: '' },
+ });
+ });
+
+ it('should dispatch correct action for order cancellation failure', async () => {
+ jest.spyOn(axios, 'delete').mockImplementation(() => Promise.reject(errResponse));
+ await cancelOrder(2)(dispatch);
+
+ expect(dispatch).toHaveBeenCalledTimes(3);
+ expect(dispatch.mock.calls[1][0]).toEqual({ type: 'CANCEL_ORDER_FAIL' });
+ expect(dispatch).toHaveBeenLastCalledWith({
+ type: 'STOP_FETCHING',
+ payload: {
+ error: true,
+ message: 'failed to cancel order',
+ },
+ });
+ });
+});
diff --git a/src/tests/components/OrderHistory.test.jsx b/src/tests/components/OrderHistory.test.jsx
index ec98abe..3d02e0d 100644
--- a/src/tests/components/OrderHistory.test.jsx
+++ b/src/tests/components/OrderHistory.test.jsx
@@ -37,6 +37,7 @@ describe('', () => {
const props = {
orders,
getUserOrderHistory: jest.fn(),
+ cancelOrder: jest.fn(),
};
it('should render correctly', () => {
@@ -47,4 +48,13 @@ describe('', () => {
expect(orderCards.length).toEqual(4);
wrapper.unmount();
});
+
+ it('should fire cancel order action creator upon order cancellation request', () => {
+ const wrapper = mount();
+ const fourthOrderCard = wrapper.find('.order-card').last();
+ fourthOrderCard.find('.cancel-order > button').simulate('click');
+
+ expect(props.cancelOrder).toHaveBeenCalled();
+ expect(props.cancelOrder).toHaveBeenCalledWith(4);
+ });
});
diff --git a/src/tests/components/__snapshots__/App.test.jsx.snap b/src/tests/components/__snapshots__/App.test.jsx.snap
index bf6db8b..8c67d5c 100644
--- a/src/tests/components/__snapshots__/App.test.jsx.snap
+++ b/src/tests/components/__snapshots__/App.test.jsx.snap
@@ -55,6 +55,7 @@ exports[`App component should render app correctly 1`] = `
>