Add stock management components to simple product

This commit is contained in:
dominik-zeglen 2020-03-19 18:15:22 +01:00
parent 0805ff052d
commit 11a10686ce
23 changed files with 256 additions and 135 deletions

View file

@ -1,96 +0,0 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import { makeStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import React from "react";
import { useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle";
import { ProductErrorFragment } from "@saleor/attributes/types/ProductErrorFragment";
import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors";
import { ProductDetails_product } from "../../types/ProductDetails";
const useStyles = makeStyles(
theme => ({
root: {
display: "grid",
gridColumnGap: theme.spacing(2),
gridTemplateColumns: "1fr 1fr"
}
}),
{ name: "ProductStock" }
);
interface ProductStockProps {
data: {
sku: string;
stockQuantity: number;
};
disabled: boolean;
errors: ProductErrorFragment[];
product: ProductDetails_product;
onChange: (event: React.ChangeEvent<any>) => void;
}
const ProductStock: React.FC<ProductStockProps> = props => {
const { data, disabled, product, onChange, errors } = props;
const classes = useStyles(props);
const intl = useIntl();
const formErrors = getFormErrors(["sku", "stockQuantity"], errors);
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Inventory",
description: "product stock, section header",
id: "productStockHeader"
})}
/>
<CardContent>
<div className={classes.root}>
<TextField
disabled={disabled}
name="sku"
label={intl.formatMessage({
defaultMessage: "SKU (Stock Keeping Unit)"
})}
value={data.sku}
onChange={onChange}
error={!!formErrors.sku}
helperText={getProductErrorMessage(formErrors.sku, intl)}
/>
<TextField
disabled={disabled}
name="stockQuantity"
label={intl.formatMessage({
defaultMessage: "Inventory",
description: "product stock",
id: "prodictStockInventoryLabel"
})}
value={data.stockQuantity}
type="number"
onChange={onChange}
helperText={
getProductErrorMessage(formErrors.stockQuantity, intl) ||
(product &&
intl.formatMessage(
{
defaultMessage: "Allocated: {quantity}",
description: "allocated product stock"
},
{
quantity: product?.variants[0].quantityAllocated
}
))
}
/>
</div>
</CardContent>
</Card>
);
};
ProductStock.displayName = "ProductStock";
export default ProductStock;

View file

@ -1,2 +0,0 @@
export { default } from "./ProductStock";
export * from "./ProductStock";

View file

@ -0,0 +1,199 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import React from "react";
import { useIntl, FormattedMessage } from "react-intl";
import makeStyles from "@material-ui/core/styles/makeStyles";
import { FormChange } from "@saleor/hooks/useForm";
import { FormsetChange, FormsetAtomicData } from "@saleor/hooks/useFormset";
import CardTitle from "@saleor/components/CardTitle";
import { getFieldError } from "@saleor/utils/errors";
import { UserError } from "@saleor/types";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import FormSpacer from "@saleor/components/FormSpacer";
import Hr from "@saleor/components/Hr";
import { renderCollection } from "@saleor/misc";
export type ProductStockInput = FormsetAtomicData<null, string>;
export interface ProductStockFormData {
sku: string;
trackInventory: boolean;
}
export interface ProductStocksProps {
data: ProductStockFormData;
disabled: boolean;
errors: UserError[];
stocks: ProductStockInput[];
onChange: FormsetChange;
onFormDataChange: FormChange;
onWarehousesEdit: () => undefined;
}
const useStyles = makeStyles(
theme => ({
colName: {},
colQuantity: {
textAlign: "right",
width: 200
},
editWarehouses: {
marginRight: -theme.spacing()
},
input: {
padding: theme.spacing(1.5),
textAlign: "right"
},
inputComponent: {
width: 100
},
quantityContainer: {
paddingTop: theme.spacing()
},
quantityHeader: {
alignItems: "center",
display: "flex",
justifyContent: "space-between"
},
skuInputContainer: {
display: "grid",
gridColumnGap: theme.spacing(3) + "px",
gridTemplateColumns: "repeat(2, 1fr)"
}
}),
{
name: "ProductStocks"
}
);
const ProductStocks: React.FC<ProductStocksProps> = ({
data,
disabled,
errors,
stocks,
onChange,
onFormDataChange,
onWarehousesEdit
}) => {
const classes = useStyles({});
const intl = useIntl();
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Inventory",
description: "product stock, section header",
id: "productStockHeader"
})}
/>
<CardContent>
<div className={classes.skuInputContainer}>
<TextField
disabled={disabled}
error={!!getFieldError(errors, "sku")}
fullWidth
helperText={getFieldError(errors, "sku")?.message}
label={intl.formatMessage({
defaultMessage: "SKU (Stock Keeping Unit)"
})}
name="sku"
onChange={onFormDataChange}
value={data.sku}
/>
</div>
<FormSpacer />
<ControlledCheckbox
checked={data.trackInventory}
name="trackInventory"
onChange={onFormDataChange}
disabled={disabled}
label={
<>
<FormattedMessage
defaultMessage="Track Inventory"
description="product inventory, checkbox"
/>
<Typography variant="caption">
<FormattedMessage defaultMessage="Active inventory tracking will automatically calculate changes of stock" />
</Typography>
</>
}
/>
</CardContent>
<Hr />
<CardContent className={classes.quantityContainer}>
<Typography>
<div className={classes.quantityHeader}>
<span>
<FormattedMessage
defaultMessage="Quantity"
description="header"
/>
</span>
<Button
className={classes.editWarehouses}
color="primary"
data-cy="edit-warehouses"
onClick={onWarehousesEdit}
>
<FormattedMessage
defaultMessage="Edit Warehouses"
description="button"
/>
</Button>
</div>
</Typography>
</CardContent>
<Table>
<TableHead>
<TableRow>
<TableCell className={classes.colName}>
<FormattedMessage
defaultMessage="Warehouse Name"
description="tabel column header"
/>
</TableCell>
<TableCell className={classes.colQuantity}>
<FormattedMessage
defaultMessage="Quantity Available"
description="tabel column header"
/>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{renderCollection(stocks, stock => (
<TableRow>
<TableCell className={classes.colName}>{stock.label}</TableCell>
<TableCell className={classes.colQuantity}>
<TextField
className={classes.inputComponent}
fullWidth
inputProps={{
className: classes.input,
min: 0,
type: "number"
}}
onChange={event => onChange(stock.id, event.target.value)}
value={stock.value}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
);
};
ProductStocks.displayName = "ProductStocks";
export default ProductStocks;

View file

@ -0,0 +1,2 @@
export * from "./ProductStocks";
export { default } from "./ProductStocks";

View file

@ -34,7 +34,8 @@ import {
getProductUpdatePageFormData,
getSelectedAttributesFromProduct,
ProductAttributeValueChoices,
ProductUpdatePageFormData
ProductUpdatePageFormData,
getStockInputFromProduct
} from "../../utils/data";
import {
createAttributeChangeHandler,
@ -45,8 +46,8 @@ import ProductDetailsForm from "../ProductDetailsForm";
import ProductImages from "../ProductImages";
import ProductOrganization from "../ProductOrganization";
import ProductPricing from "../ProductPricing";
import ProductStock from "../ProductStock";
import ProductVariants from "../ProductVariants";
import ProductStocks, { ProductStockInput } from "../ProductStocks";
export interface ProductUpdatePageProps extends ListActions {
errors: ProductErrorFragment[];
@ -71,7 +72,6 @@ export interface ProductUpdatePageProps extends ListActions {
onImageEdit?(id: string);
onImageReorder?(event: { oldIndex: number; newIndex: number });
onImageUpload(file: File);
onProductShow?();
onSeoClick?();
onSubmit?(data: ProductUpdatePageSubmitData);
onVariantAdd?();
@ -80,6 +80,7 @@ export interface ProductUpdatePageProps extends ListActions {
export interface ProductUpdatePageSubmitData extends ProductUpdatePageFormData {
attributes: ProductAttributeInput[];
collections: string[];
stocks: ProductStockInput[];
}
export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
@ -120,9 +121,13 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
() => getAttributeInputFromProduct(product),
[product]
);
const stockInput = React.useMemo(() => getStockInputFromProduct(product), [
product
]);
const { change: changeAttributeData, data: attributes } = useFormset(
attributeInput
);
const { change: changeStockData, data: stocks } = useFormset(stockInput);
const [selectedAttributes, setSelectedAttributes] = useStateFromProps<
ProductAttributeValueChoices[]
@ -149,6 +154,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
const handleSubmit = (data: ProductUpdatePageFormData) =>
onSubmit({
attributes,
stocks,
...data
});
@ -239,12 +245,13 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
toggleAll={toggleAll}
/>
) : (
<ProductStock
<ProductStocks
data={data}
disabled={disabled}
product={product}
onChange={change}
errors={errors}
stocks={stocks}
onChange={changeStockData}
onFormDataChange={change}
/>
)}
<CardSpacer />

View file

@ -22,7 +22,7 @@ import { maybe, renderCollection } from "../../../misc";
import { ListActions } from "../../../types";
import {
ProductDetails_product_variants,
ProductDetails_product_variants_stock_warehouse
ProductDetails_product_variants_stocks_warehouse
} from "../../types/ProductDetails";
import { ProductVariant_costPrice } from "../../types/ProductVariant";
@ -39,11 +39,11 @@ function getWarehouseChoices(
value: null
},
...variants
.reduce<ProductDetails_product_variants_stock_warehouse[]>(
.reduce<ProductDetails_product_variants_stocks_warehouse[]>(
(warehouses, variant) => [
...warehouses,
...variant.stock.reduce<
ProductDetails_product_variants_stock_warehouse[]
...variant.stocks.reduce<
ProductDetails_product_variants_stocks_warehouse[]
>((variantStocks, stock) => {
if (!!warehouses.find(w => w.id === stock.warehouse.id)) {
return variantStocks;
@ -118,7 +118,7 @@ function getAvailabilityLabel(
variant: ProductDetails_product_variants,
numAvailable: number
): string {
const variantStock = variant.stock.find(s => s.warehouse.id === warehouse);
const variantStock = variant.stocks.find(s => s.warehouse.id === warehouse);
if (!!warehouse) {
if (!!variantStock) {
@ -155,7 +155,7 @@ function getAvailabilityLabel(
},
{
numAvailable,
numLocations: variant.stock.length
numLocations: variant.stocks.length
}
);
} else {
@ -295,8 +295,8 @@ export const ProductVariants: React.FC<ProductVariantsProps> = props => {
{renderCollection(variants, variant => {
const isSelected = variant ? isChecked(variant.id) : false;
const numAvailable =
variant && variant.stock
? variant.stock.reduce((acc, s) => acc + s.quantity, 0)
variant && variant.stocks
? variant.stocks.reduce((acc, s) => acc + s.quantity, 0)
: null;
return (

View file

@ -260,10 +260,8 @@ export const product: (
amount: 678.78,
currency: "USD"
},
quantity: 12,
quantityAllocated: 1,
sku: "87192-94370",
stock: [
stocks: [
{
__typename: "Stock",
id: "1",
@ -277,7 +275,7 @@ export const product: (
warehouse: warehouseList[1]
}
],
stockQuantity: 48
trackInventory: true
},
{
__typename: "ProductVariant",
@ -297,10 +295,8 @@ export const product: (
margin: 7,
name: "silver",
priceOverride: null,
quantity: 12,
quantityAllocated: 1,
sku: "69055-15190",
stock: [
stocks: [
{
__typename: "Stock",
id: "1",
@ -308,7 +304,7 @@ export const product: (
warehouse: warehouseList[0]
}
],
stockQuantity: 14
trackInventory: false
}
]
});

View file

@ -158,6 +158,7 @@ export const productFragmentDetails = gql`
stocks {
...StockFragment
}
trackInventory
}
productType {
id
@ -225,6 +226,7 @@ export const fragmentVariant = gql`
stocks {
...StockFragment
}
trackInventory
}
`;

View file

@ -148,6 +148,7 @@ export interface Product_variants {
priceOverride: Product_variants_priceOverride | null;
margin: number | null;
stocks: (Product_variants_stocks | null)[] | null;
trackInventory: boolean;
}
export interface Product_productType {

View file

@ -154,6 +154,7 @@ export interface ProductCreate_productCreate_product_variants {
priceOverride: ProductCreate_productCreate_product_variants_priceOverride | null;
margin: number | null;
stocks: (ProductCreate_productCreate_product_variants_stocks | null)[] | null;
trackInventory: boolean;
}
export interface ProductCreate_productCreate_product_productType {

View file

@ -148,6 +148,7 @@ export interface ProductDetails_product_variants {
priceOverride: ProductDetails_product_variants_priceOverride | null;
margin: number | null;
stocks: (ProductDetails_product_variants_stocks | null)[] | null;
trackInventory: boolean;
}
export interface ProductDetails_product_productType_variantAttributes_values {

View file

@ -154,6 +154,7 @@ export interface ProductImageCreate_productImageCreate_product_variants {
priceOverride: ProductImageCreate_productImageCreate_product_variants_priceOverride | null;
margin: number | null;
stocks: (ProductImageCreate_productImageCreate_product_variants_stocks | null)[] | null;
trackInventory: boolean;
}
export interface ProductImageCreate_productImageCreate_product_productType {

View file

@ -154,6 +154,7 @@ export interface ProductImageUpdate_productImageUpdate_product_variants {
priceOverride: ProductImageUpdate_productImageUpdate_product_variants_priceOverride | null;
margin: number | null;
stocks: (ProductImageUpdate_productImageUpdate_product_variants_stocks | null)[] | null;
trackInventory: boolean;
}
export interface ProductImageUpdate_productImageUpdate_product_productType {

View file

@ -154,6 +154,7 @@ export interface ProductUpdate_productUpdate_product_variants {
priceOverride: ProductUpdate_productUpdate_product_variants_priceOverride | null;
margin: number | null;
stocks: (ProductUpdate_productUpdate_product_variants_stocks | null)[] | null;
trackInventory: boolean;
}
export interface ProductUpdate_productUpdate_product_productType {

View file

@ -113,4 +113,5 @@ export interface ProductVariant {
product: ProductVariant_product;
sku: string;
stocks: (ProductVariant_stocks | null)[] | null;
trackInventory: boolean;
}

View file

@ -113,6 +113,7 @@ export interface ProductVariantDetails_productVariant {
product: ProductVariantDetails_productVariant_product;
sku: string;
stocks: (ProductVariantDetails_productVariant_stocks | null)[] | null;
trackInventory: boolean;
}
export interface ProductVariantDetails {

View file

@ -154,6 +154,7 @@ export interface SimpleProductUpdate_productUpdate_product_variants {
priceOverride: SimpleProductUpdate_productUpdate_product_variants_priceOverride | null;
margin: number | null;
stocks: (SimpleProductUpdate_productUpdate_product_variants_stocks | null)[] | null;
trackInventory: boolean;
}
export interface SimpleProductUpdate_productUpdate_product_productType {
@ -305,6 +306,7 @@ export interface SimpleProductUpdate_productVariantUpdate_productVariant {
product: SimpleProductUpdate_productVariantUpdate_productVariant_product;
sku: string;
stocks: (SimpleProductUpdate_productVariantUpdate_productVariant_stocks | null)[] | null;
trackInventory: boolean;
}
export interface SimpleProductUpdate_productVariantUpdate {

View file

@ -121,6 +121,7 @@ export interface VariantCreate_productVariantCreate_productVariant {
product: VariantCreate_productVariantCreate_productVariant_product;
sku: string;
stocks: (VariantCreate_productVariantCreate_productVariant_stocks | null)[] | null;
trackInventory: boolean;
}
export interface VariantCreate_productVariantCreate {

View file

@ -121,6 +121,7 @@ export interface VariantImageAssign_variantImageAssign_productVariant {
product: VariantImageAssign_variantImageAssign_productVariant_product;
sku: string;
stocks: (VariantImageAssign_variantImageAssign_productVariant_stocks | null)[] | null;
trackInventory: boolean;
}
export interface VariantImageAssign_variantImageAssign {

View file

@ -121,6 +121,7 @@ export interface VariantImageUnassign_variantImageUnassign_productVariant {
product: VariantImageUnassign_variantImageUnassign_productVariant_product;
sku: string;
stocks: (VariantImageUnassign_variantImageUnassign_productVariant_stocks | null)[] | null;
trackInventory: boolean;
}
export interface VariantImageUnassign_variantImageUnassign {

View file

@ -121,6 +121,7 @@ export interface VariantUpdate_productVariantUpdate_productVariant {
product: VariantUpdate_productVariantUpdate_productVariant_product;
sku: string;
stocks: (VariantUpdate_productVariantUpdate_productVariant_stocks | null)[] | null;
trackInventory: boolean;
}
export interface VariantUpdate_productVariantUpdate {

View file

@ -13,6 +13,7 @@ import { ProductAttributeInput } from "../components/ProductAttributes";
import { VariantAttributeInput } from "../components/ProductVariantAttributes";
import { ProductVariant } from "../types/ProductVariant";
import { ProductVariantCreateData_product } from "../types/ProductVariantCreateData";
import { ProductStockInput } from "../components/ProductStocks";
export interface Collection {
id: string;
@ -117,6 +118,17 @@ export function getVariantAttributeInputFromProduct(
);
}
export function getStockInputFromProduct(
product: ProductDetails_product
): ProductStockInput[] {
return product?.variants[0].stocks.map(stock => ({
data: null,
id: stock.warehouse.id,
label: stock.warehouse.name,
value: stock.quantity.toString()
}));
}
export function getCollectionInput(
productCollections: ProductDetails_product_collections[]
): Collection[] {
@ -153,7 +165,7 @@ export interface ProductUpdatePageFormData {
seoDescription: string;
seoTitle: string;
sku: string;
stockQuantity: number;
trackInventory: boolean;
}
export function getProductUpdatePageFormData(
@ -183,14 +195,6 @@ export function getProductUpdatePageFormData(
: undefined,
""
),
stockQuantity: maybe(
() =>
product.productType.hasVariants
? undefined
: variants && variants[0]
? variants[0].quantity
: undefined,
0
)
trackInventory: !!product?.variants[0]?.trackInventory
};
}

View file

@ -232,11 +232,6 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
variants={maybe(() => product.variants)}
onBack={handleBack}
onDelete={() => openModal("remove")}
onProductShow={() => {
if (product) {
window.open(product.url);
}
}}
onImageReorder={handleImageReorder}
onSubmit={handleSubmit}
onVariantAdd={handleVariantAdd}