Skip to content

Commit

Permalink
Checkout options (#319)
Browse files Browse the repository at this point in the history
Add context menu to cart items
  • Loading branch information
Serunde authored and jimbo committed Nov 19, 2018
1 parent 1541400 commit 7a4cbf3
Show file tree
Hide file tree
Showing 24 changed files with 862 additions and 44 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"prettier": "prettier --write '@(packages|scripts)/**/*.@(js|css)' '*.js'",
"prettier:check": "prettier --list-different '@(packages|scripts)/**/*.@(js|css)' '*.js'",
"stage:venia": "cd packages/venia-concept && npm start; cd - >/dev/null",
"storybook:venia": "cd packages/venia-concept && npm run storybook",
"test": "jest",
"test:ci": "npm run -s test -- -i --json --outputFile=test-results.json",
"test:debug": "node --inspect-brk node_modules/.bin/jest -i",
Expand Down
8 changes: 8 additions & 0 deletions packages/venia-concept/.storybook/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { configure } from '@storybook/react';

function loadStories() {
const context = require.context('../src', true, /__stories__\/.+\.js$/);
context.keys().forEach(context);
}

configure(loadStories, module);
63 changes: 63 additions & 0 deletions packages/venia-concept/.storybook/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const path = require('path');

const configureBabel = require('../babel.config.js');
const babelOptions = configureBabel('development');
console.log(babelOptions);

const base_config = require('./webpack.config.js');

const themePaths = {
src: path.resolve(__dirname, '../src'),
assets: path.resolve(__dirname, '../web'),
output: path.resolve(__dirname, '../web/js'),
node: path.resolve(__dirname, '../../../')
};

console.log(themePaths.node);

const testPath = path.resolve('../');

module.exports = (storybookBaseConfig, configType) => {
storybookBaseConfig.module.rules.push({
include: [themePaths.src],
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: { ...babelOptions, cacheDirectory: true }
}
]
});

storybookBaseConfig.module.rules.push({
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
localIdentName: '[name]-[local]-[hash:base64:3]',
modules: true
}
}
]
});

storybookBaseConfig.module.rules.push({
test: /\.(jpg|svg)$/,
use: [
{
loader: 'file-loader',
options: {}
}
]
});

storybookBaseConfig.resolve.alias = {
src: themePaths.src
};
storybookBaseConfig.resolve.modules = ['node_modules'];

return storybookBaseConfig;
};
31 changes: 31 additions & 0 deletions packages/venia-concept/src/actions/cart/__tests__/actions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,37 @@ test('addItem.receive() returns a proper action object', () => {
});
});

test('removeItem.request.toString() returns the proper action type', () => {
expect(actions.removeItem.request.toString()).toBe(
'CART/REMOVE_ITEM/REQUEST'
);
});

test('removeItem.request() returns a proper action object', () => {
expect(actions.removeItem.request(payload)).toEqual({
type: 'CART/REMOVE_ITEM/REQUEST',
payload
});
});

test('removeItem.receive.toString() returns the proper action type', () => {
expect(actions.removeItem.receive.toString()).toBe(
'CART/REMOVE_ITEM/RECEIVE'
);
});

test('removeItem.receive() returns a proper action object', () => {
expect(actions.removeItem.receive(payload)).toEqual({
type: 'CART/REMOVE_ITEM/RECEIVE',
payload
});
expect(actions.removeItem.receive(error)).toEqual({
type: 'CART/REMOVE_ITEM/RECEIVE',
payload: error,
error: true
});
});

test('getGuestCart.request.toString() returns the proper action type', () => {
expect(actions.getGuestCart.request.toString()).toBe(
'CART/GET_GUEST_CART/REQUEST'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import actions from '../actions';
import {
addItemToCart,
removeItemFromCart,
createGuestCart,
getCartDetails,
toggleCart
Expand Down Expand Up @@ -371,6 +372,85 @@ test('addItemToCart opens drawer and gets cart details on success', async () =>
expect(request).toHaveBeenCalledTimes(3);
});

test('removeItemFromCart() returns a thunk', () => {
expect(removeItemFromCart({})).toBeInstanceOf(Function);
});

test('removeItemFromCart thunk returns undefined', async () => {
const result = await removeItemFromCart({})(...thunkArgs);

expect(result).toBeUndefined();
});

test('removeItemFromCart thunk dispatches actions on success', async () => {
const payload = { item: 'ITEM' };
const cartItem = 'CART_ITEM';

request.mockResolvedValueOnce(cartItem);
await removeItemFromCart(payload)(...thunkArgs);

expect(dispatch).toHaveBeenNthCalledWith(
1,
actions.removeItem.request(payload)
);
expect(dispatch).toHaveBeenNthCalledWith(
2,
actions.removeItem.receive({ cartItem, cartItemCount: 0, ...payload })
);
expect(dispatch).toHaveBeenNthCalledWith(3, expect.any(Function));
expect(dispatch).toHaveBeenCalledTimes(3);
});

test('removeItemFromCart thunk dispatches special failure if guestCartId is not present', async () => {
const payload = { item: 'ITEM' };
const error = new Error('Missing required information: guestCartId');
error.noGuestCartId = true;
getState.mockImplementationOnce(() => ({ cart: {} }));
await removeItemFromCart(payload)(...thunkArgs);
expect(mockRemoveItem).toHaveBeenCalledWith('guestCartId');
expect(dispatch).toHaveBeenNthCalledWith(
1,
actions.removeItem.request(payload)
);
expect(dispatch).toHaveBeenNthCalledWith(
2,
actions.removeItem.receive(error)
);
expect(dispatch).toHaveBeenNthCalledWith(3, expect.any(Function));
});

test('removeItemFromCart tries to recreate a guest cart on 404 failure', async () => {
getState.mockImplementationOnce(() => ({
cart: { guestCartId: 'OLD_AND_BUSTED' }
}));
const payload = { item: 'ITEM' };
const error = new Error('ERROR');
error.response = {
status: 404
};

request.mockRejectedValueOnce(error);

await removeItemFromCart(payload)(...thunkArgs);

expect(request).toHaveBeenCalledTimes(2);
});

test('removeItemFromCart resets the guest cart when removing the last item in the cart', async () => {
getState.mockImplementationOnce(() => ({
cart: { guestCartId: 'CART', details: { items_count: 1 } }
}));
let payload = { item: 'ITEM' };

// removeItemFromCart() calls storage.removeItem() to clear the guestCartId
// but only if there's 1 item left in the cart
mockRemoveItem.mockImplementationOnce(() => {});

await removeItemFromCart(payload)(...thunkArgs);

expect(mockRemoveItem).toHaveBeenCalled();
});

test('getCartDetails() returns a thunk', () => {
expect(getCartDetails()).toBeInstanceOf(Function);
});
Expand Down
4 changes: 4 additions & 0 deletions packages/venia-concept/src/actions/cart/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ const actionMap = {
REQUEST: null,
RECEIVE: null
},
REMOVE_ITEM: {
REQUEST: null,
RECEIVE: null
},
GET_GUEST_CART: {
REQUEST: null,
RECEIVE: null
Expand Down
58 changes: 58 additions & 0 deletions packages/venia-concept/src/actions/cart/asyncActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,64 @@ export const addItemToCart = (payload = {}) => {
};
};

export const removeItemFromCart = payload => {
const { item } = payload;

return async function thunk(dispatch, getState) {
dispatch(actions.removeItem.request(payload));

try {
const { cart } = getState();
const { guestCartId } = cart;
const cartItemCount = cart.details ? cart.details.items_count : 0;

if (!guestCartId) {
const missingGuestCartError = new Error(
'Missing required information: guestCartId'
);
missingGuestCartError.noGuestCartId = true;
throw missingGuestCartError;
}

const cartItem = await request(
`/rest/V1/guest-carts/${guestCartId}/items/${item.item_id}`,
{
method: 'DELETE'
}
);
// When removing the last item in the cart, perform a reset
// to prevent a bug where the next item added to the cart has
// a price of 0
if (cartItemCount == 1) {
await clearGuestCartId();
}

dispatch(
actions.removeItem.receive({ cartItem, item, cartItemCount })
);
} catch (error) {
const { response, noGuestCartId } = error;

dispatch(actions.removeItem.receive(error));

// check if the guest cart has expired
if (noGuestCartId || (response && response.status === 404)) {
// if so, then delete the cached ID...
// in contrast to the save, make sure storage deletion is
// complete before dispatching the error--you don't want an
// upstream action to try and reuse the known-bad ID.
await clearGuestCartId();
// then create a new one
await dispatch(createGuestCart());
// then retry this operation
return thunk(...arguments);
}
}

await Promise.all([dispatch(getCartDetails({ forceRefresh: true }))]);
};
};

export const getCartDetails = (payload = {}) => {
const { forceRefresh } = payload;

Expand Down
12 changes: 6 additions & 6 deletions packages/venia-concept/src/components/Gallery/gallery.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,6 @@
grid-template-columns: repeat(3, 1fr);
}

@media (max-width: 640px) {
.items {
grid-template-columns: repeat(2, 1fr);
}
}

.pagination {
display: grid;
grid-area: pagination;
Expand All @@ -40,3 +34,9 @@
justify-content: center;
padding-top: 1rem;
}

@media (max-width: 640px) {
.items {
grid-template-columns: repeat(2, 1fr);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import { storiesOf } from '@storybook/react';

import Kebab from '../kebab';
import Section from '../section';
import 'src/index.css';

const stories = storiesOf('Mini Cart/Kebab', module);

const styles = {
width: '150px',
height: '150px',
display: 'grid'
};

stories.add('Kebab Closed', () => (
<div style={styles}>
<Kebab />
</div>
));

stories.add('Kebab Open', () => (
<div style={styles}>
<Kebab isOpen={true}>
<Section icon="heart" text="Section 1" />
<Section icon="x" text="Section 2" />
<Section icon="chevron-up" text="Section 3" />
<Section text="Non-icon Section" />
</Kebab>
</div>
));
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';
import { storiesOf } from '@storybook/react';

import ProductList from '../productList';
import 'src/index.css';

const stories = storiesOf('Mini Cart/Product List', module);

const items = [
{
item_id: 1,
name: 'Product 1',
price: 10,
qty: 1,
sku: 'TEST1',
image: 'test.jpg'
},
{
item_id: 2,
name: 'Product 2',
price: 5,
qty: 1,
sku: 'TEST2',
image: 'test.jpg'
},
{
item_id: 3,
name: 'Product 3',
price: 15,
qty: 1,
sku: 'TEST3',
image: 'test.jpg'
}
];

stories.add('Product List With Kebab', () => (
<ProductList
items={items}
currencyCode="NZD"
removeItemFromCart={() => {}}
showEditPanel={() => {}}
/>
));
Loading

1 comment on commit 7a4cbf3

@vercel
Copy link

@vercel vercel bot commented on 7a4cbf3 Nov 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully aliased the URL https://magento-venia-vmqykrsgwi.now.sh to the following alias.

Please sign in to comment.