Skip to content

Commit

Permalink
Merge pull request #380 from reactioncommerce/feat-kieckhafer-invento…
Browse files Browse the repository at this point in the history
…ryStatusComponent

feat: add new inventoryStatus component
  • Loading branch information
machikoyasuda authored Dec 13, 2018
2 parents 8ba70d8 + b3b5482 commit b234f72
Show file tree
Hide file tree
Showing 18 changed files with 436 additions and 3 deletions.
90 changes: 90 additions & 0 deletions package/src/components/InventoryStatus/v1/InventoryStatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import styled from "styled-components";
import { withComponents } from "@reactioncommerce/components-context";
import { addTypographyStyles, CustomPropTypes } from "../../../utils";
import { STATUS_LABELS, inventoryStatus } from "./utils";

const SoldOutSpan = styled.div`
${addTypographyStyles("StockWarning", "labelText")}
`;

const DefaultSpan = styled.div`
${addTypographyStyles("", "labelText")}
`;

class InventoryStatus extends Component {
static propTypes = {
/**
* You can provide a `className` prop that will be applied to the outermost DOM element
* rendered by this component. We do not recommend using this for styling purposes, but
* it can be useful as a selector in some situations.
*/
className: PropTypes.string,
/**
* If you've set up a components context using
* [@reactioncommerce/components-context](https://github.com/reactioncommerce/components-context)
* (recommended), then this prop will come from there automatically. If you have not
* set up a components context or you want to override one of the components in a
* single spot, you can pass in the components prop directly.
*/
components: PropTypes.shape({
/**
* Pass either the Reaction StockWarning component or your own component that
* accepts compatible props.
*/
StockWarning: CustomPropTypes.component.isRequired
}).isRequired,
/**
* The product, whose properties determine which badge(s) to display
*/
product: PropTypes.shape({
inventoryAvailableToSell: PropTypes.number,
isBackorder: PropTypes.bool,
isLowQuantity: PropTypes.bool,
isSoldOut: PropTypes.bool
}),
/**
* Labels to use for the various badges
*/
statusLabels: PropTypes.shape({
BACKORDER: PropTypes.string,
LOW_QUANTITY: PropTypes.string,
SOLD_OUT: PropTypes.string
})
};

static defaultProps = {
statusLabels: STATUS_LABELS
};

render() {
const { className, components, product, statusLabels } = this.props;
const { StockWarning } = components || {};

const status = inventoryStatus(product, statusLabels);

if (!status) return null;

if (status.type && status.type === "LOW_QUANTITY") {
return (
<StockWarning
inventoryQuantity={product.inventoryAvailableToSell}
isLowInventoryQuantity={product.isLowQuantity}
/>
);
}

if (status.type && status.type === "SOLD_OUT") {
return (
<SoldOutSpan className={className}>{status.label}</SoldOutSpan>
);
}

return (
<DefaultSpan className={className}>{status.label}</DefaultSpan>
);
}
}

export default withComponents(InventoryStatus);
62 changes: 62 additions & 0 deletions package/src/components/InventoryStatus/v1/InventoryStatus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
### Overview
The `InventoryStatus` displays a low inventory warning when the `isLowQuantity` prop is true.

### Usage

An inventory warning will be rendered when the `isLowQuantity` prop is `true`, and does not render when a product has a normal inventory level.

#### Backorder
```jsx
const productData = {
isBackorder: true,
isLowQuantity: true,
isSoldOut: true,
inventoryAvailableToSell: 0
};

<InventoryStatus product={productData} />
```

#### Low inventory
```jsx
const productData = {
isBackorder: false,
isLowQuantity: true,
isSoldOut: false,
inventoryAvailableToSell: 4
};

<InventoryStatus product={productData} />
```

#### Regular inventory
```jsx
const productData = {
isBackorder: false,
isLowQuantity: false,
isSoldOut: false,
inventoryAvailableToSell: 4
};

<InventoryStatus product={productData} />
```

#### Sold out
```jsx
const productData = {
isBackorder: false,
isLowQuantity: true,
isSoldOut: true,
inventoryAvailableToSell: 0
};

<InventoryStatus product={productData} />
```

### Theme

See [Theming Components](./#!/Theming%20Components).

#### Typography

- The text uses `labelText` style with `rui_components.InventoryStatus` override
74 changes: 74 additions & 0 deletions package/src/components/InventoryStatus/v1/InventoryStatus.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from "react";
import renderer from "react-test-renderer";
import checkPropTypes from "check-prop-types";
import mockComponents from "../../../tests/mockComponents";
import InventoryStatus from "./InventoryStatus";

test("Displays error warning about required props", () => {
const errorMessage = checkPropTypes(InventoryStatus.propTypes, {});
expect(errorMessage).toMatchSnapshot();
});

test("Renders backorder notification when inventory is sold out and backorder is allowed", () => {
const productData = {
isBackorder: true,
isLowQuantity: true,
isSoldOut: true,
inventoryAvailableToSell: 0
};

const component = renderer.create((
<InventoryStatus components={mockComponents} product={productData} />
));

const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});

test("Renders low inventory notification when inventory is lower than threshold", () => {
const productData = {
isBackorder: false,
isLowQuantity: true,
isSoldOut: false,
inventoryAvailableToSell: 6
};

const component = renderer.create((
<InventoryStatus components={mockComponents} product={productData} />
));

const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});

test("Renders nothing when inventory is ready to be sold", () => {
const productData = {
isBackorder: false,
isLowQuantity: false,
isSoldOut: false,
inventoryAvailableToSell: 4
};

const component = renderer.create((
<InventoryStatus components={mockComponents} product={productData} />
));

const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});

test("Renders sold out notification when inventory is sold out and backorder is not allowed", () => {
const productData = {
isBackorder: false,
isLowQuantity: true,
isSoldOut: true,
inventoryAvailableToSell: 0
};

const component = renderer.create((
<InventoryStatus components={mockComponents} product={productData} />
));

const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Displays error warning about required props 1`] = `undefined`;

exports[`Renders backorder notification when inventory is sold out and backorder is allowed 1`] = `
.c0 {
-webkit-font-smoothing: antialiased;
color: #505558;
font-family: 'Source Sans Pro','Helvetica Neue',Helvetica,sans-serif;
font-size: 14px;
font-style: normal;
font-stretch: normal;
font-weight: 400;
-webkit-letter-spacing: .02em;
-moz-letter-spacing: .02em;
-ms-letter-spacing: .02em;
letter-spacing: .02em;
line-height: 1.25;
}
<div
className="c0"
>
Backordered - ships when available
</div>
`;

exports[`Renders low inventory notification when inventory is lower than threshold 1`] = `"StockWarning({\\"inventoryQuantity\\":6,\\"isLowInventoryQuantity\\":true})"`;

exports[`Renders nothing when inventory is ready to be sold 1`] = `null`;

exports[`Renders sold out notification when inventory is sold out and backorder is not allowed 1`] = `
.c0 {
-webkit-font-smoothing: antialiased;
color: #cd3f4c;
font-family: 'Source Sans Pro','Helvetica Neue',Helvetica,sans-serif;
font-size: 14px;
font-style: normal;
font-stretch: normal;
font-weight: 400;
-webkit-letter-spacing: .02em;
-moz-letter-spacing: .02em;
-ms-letter-spacing: .02em;
letter-spacing: .02em;
line-height: 1.25;
}
<div
className="c0"
>
Out of stock
</div>
`;
1 change: 1 addition & 0 deletions package/src/components/InventoryStatus/v1/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./InventoryStatus";
5 changes: 5 additions & 0 deletions package/src/components/InventoryStatus/v1/utils/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { default as STATUS_TYPES } from "./statusTypes";
export { default as STATUS_LABELS } from "./statusLabels";
export { default as inventoryStatus } from "./inventoryStatus";

export { default as isProductLowQuantity } from "./isProductLowQuantity";
22 changes: 22 additions & 0 deletions package/src/components/InventoryStatus/v1/utils/inventoryStatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { STATUS_TYPES } from "./";

/**
* Determines a product's badge status
*
* @param {Object} product - The product
* @param {Object} statusLabels - Labels to use for badges
* @returns {Object} - The computed product status
*/
export default function inventoryStatus(product, statusLabels) {
let status;

if (product.isSoldOut && product.isBackorder) {
status = { type: STATUS_TYPES.BACKORDER, label: statusLabels.BACKORDER };
} else if (product.isSoldOut && !product.isBackorder) {
status = { type: STATUS_TYPES.SOLD_OUT, label: statusLabels.SOLD_OUT };
} else if (product.isLowQuantity && !product.isSoldOut) {
status = { type: STATUS_TYPES.LOW_QUANTITY, label: statusLabels.LOW_QUANTITY };
}

return status;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import inventoryStatus from "./inventoryStatus";
import STATUS_TYPES from "./statusTypes";
import STATUS_LABELS from "./statusLabels";

const backorderProduct = { isSoldOut: true, isBackorder: true };
const soldOutProduct = { isSoldOut: true, isBackorder: false };
const isLowQuantity = { isLowQuantity: true };


test("inventoryStatus util should return `backorder` status", () => {
const callFunction = inventoryStatus(backorderProduct, STATUS_LABELS);

expect(typeof inventoryStatus).toBe("function");
expect(callFunction).toEqual({ type: STATUS_TYPES.BACKORDER, label: "Backordered - ships when available" });
});

test("inventoryStatus util should return `sold out` status", () => {
const callFunction = inventoryStatus(soldOutProduct, STATUS_LABELS);

expect(typeof inventoryStatus).toBe("function");
expect(callFunction).toEqual({ type: STATUS_TYPES.SOLD_OUT, label: "Out of stock" });
});

test("inventoryStatus util should return `low inventory` status", () => {
const callFunction = inventoryStatus(isLowQuantity, STATUS_LABELS);

expect(typeof inventoryStatus).toBe("function");
expect(callFunction).toEqual({ type: STATUS_TYPES.LOW_QUANTITY, label: "Low Inventory" });
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Determines if a product is a best seller.
* TODO: this is a placeholder, as we don't have "Best Seller" at this moment
* https://github.com/reactioncommerce/reaction-next-starterkit/issues/130
*
* @param {Object} product - The product
* @returns {Boolean} - Indicates whether the product is a best seller
*/
export default function isProductBestseller(product) {
return product.isBestseller || false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import isProductBestseller from "./isProductBestseller";

const isBestseller = { isBestseller: true };
const isNotBestseller = { isBestseller: false };

test("isProductBestseller should return false", () => {
const callFunction = isProductBestseller(isNotBestseller);

expect(typeof isProductBestseller).toBe("function");
expect(callFunction).toEqual(false);
});

test("isProductBestseller should return true", () => {
const callFunction = isProductBestseller(isBestseller);

expect(typeof isProductBestseller).toBe("function");
expect(callFunction).toEqual(true);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Determines if a product has low inventory.
*
* @param {Object} product - The product
* @returns {Boolean} - Indicates whether the product has low inventory
*/
export default function isProductLowQuantity(product) {
return product.isLowQuantity && !product.isSoldOut;
}
Loading

0 comments on commit b234f72

Please sign in to comment.