Add error handling

This commit is contained in:
dominik-zeglen 2019-10-02 15:34:34 +02:00
parent 6190c4ff45
commit 7bb26784e6
26 changed files with 1701 additions and 946 deletions

View file

@ -60,6 +60,7 @@ export interface ProductUpdatePageProps extends ListActions {
saveButtonBarState: ConfirmButtonTransitionState;
fetchCategories: (query: string) => void;
fetchCollections: (query: string) => void;
onVariantsAdd: () => void;
onVariantShow: (id: string) => () => void;
onImageDelete: (id: string) => () => void;
onBack?();
@ -100,6 +101,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
onSeoClick,
onSubmit,
onVariantAdd,
onVariantsAdd,
onVariantShow,
isChecked,
selected,
@ -236,6 +238,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
fallbackPrice={product ? product.basePrice : undefined}
onRowClick={onVariantShow}
onVariantAdd={onVariantAdd}
onVariantsAdd={onVariantsAdd}
toolbar={toolbar}
isChecked={isChecked}
selected={selected}

View file

@ -4,6 +4,8 @@ import { storiesOf } from "@storybook/react";
import React from "react";
import { attributes } from "@saleor/attributes/fixtures";
import { ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors } from "@saleor/products/types/ProductVariantBulkCreate";
import { ProductErrorCode } from "@saleor/types/globalTypes";
import Decorator from "../../../storybook/Decorator";
import { createVariants } from "./createVariants";
import { AllOrAttribute } from "./form";
@ -19,7 +21,7 @@ const price: AllOrAttribute = {
attribute: selectedAttributes[1].id,
value: "2.79",
values: selectedAttributes[1].values.map((attribute, attributeIndex) => ({
id: attribute.id,
slug: attribute.slug,
value: (attributeIndex + 4).toFixed(2)
}))
};
@ -29,7 +31,7 @@ const stock: AllOrAttribute = {
attribute: selectedAttributes[1].id,
value: "8",
values: selectedAttributes[1].values.map((attribute, attributeIndex) => ({
id: attribute.id,
slug: attribute.slug,
value: (selectedAttributes.length * 10 - attributeIndex).toString()
}))
};
@ -37,10 +39,20 @@ const stock: AllOrAttribute = {
const dataAttributes = selectedAttributes.map(attribute => ({
id: attribute.id,
values: attribute.values
.map(value => value.id)
.map(value => value.slug)
.filter((_, valueIndex) => valueIndex % 2 !== 1)
}));
const errors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[] = [
{
__typename: "BulkProductError",
code: ProductErrorCode.UNIQUE,
field: "sku",
index: 3,
message: "Duplicated SKU."
}
];
const props: ProductVariantCreateContentProps = {
attributes,
currencySymbol: "USD",
@ -56,6 +68,7 @@ const props: ProductVariantCreateContentProps = {
})
},
dispatchFormDataAction: () => undefined,
errors: [],
step: "attributes"
};
@ -78,9 +91,26 @@ storiesOf("Views / Products / Create multiple variants", module)
))
.add("prices and SKU", () => (
<ProductVariantCreateContent {...props} step="prices" />
));
storiesOf("Views / Products / Create multiple variants / summary", module)
.addDecorator(storyFn => (
<Card
style={{
margin: "auto",
overflow: "visible",
width: 800
}}
>
<CardContent>{storyFn()}</CardContent>
</Card>
))
.add("summary", () => (
.addDecorator(Decorator)
.add("default", () => (
<ProductVariantCreateContent {...props} step="summary" />
))
.add("errors", () => (
<ProductVariantCreateContent {...props} step="summary" errors={errors} />
));
storiesOf("Views / Products / Create multiple variants", module)

View file

@ -1,4 +1,3 @@
import { Theme } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
@ -18,14 +17,14 @@ export interface ProductVariantCreateAttributesProps {
onAttributeClick: (id: string) => void;
}
const useStyles = makeStyles((theme: Theme) => ({
const useStyles = makeStyles({
checkboxCell: {
paddingLeft: 0
},
wideCell: {
width: "100%"
}
}));
});
const ProductVariantCreateAttributes: React.FC<
ProductVariantCreateAttributesProps

View file

@ -2,6 +2,7 @@ import React from "react";
import { makeStyles } from "@material-ui/styles";
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
import { ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors } from "@saleor/products/types/ProductVariantBulkCreate";
import { isSelected } from "@saleor/utils/lists";
import { ProductVariantCreateFormData } from "./form";
import ProductVariantCreateAttributes from "./ProductVariantCreateAttributes";
@ -24,6 +25,7 @@ export interface ProductVariantCreateContentProps {
currencySymbol: string;
data: ProductVariantCreateFormData;
dispatchFormDataAction: React.Dispatch<ProductVariantCreateReducerAction>;
errors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[];
step: ProductVariantCreateStep;
}
@ -35,6 +37,7 @@ const ProductVariantCreateContent: React.FC<
currencySymbol,
data,
dispatchFormDataAction,
errors,
step
} = props;
const classes = useStyles(props);
@ -121,6 +124,7 @@ const ProductVariantCreateContent: React.FC<
attributes={selectedAttributes}
currencySymbol={currencySymbol}
data={data}
errors={errors}
onVariantDataChange={(variantIndex, field, value) =>
dispatchFormDataAction({
field,

View file

@ -7,6 +7,8 @@ import { Theme } from "@material-ui/core/styles";
import { makeStyles } from "@material-ui/styles";
import React from "react";
import { FormattedMessage } from "react-intl";
import { ProductVariantBulkCreateInput } from "../../../types/globalTypes";
import { initialForm, ProductVariantCreateFormData } from "./form";
import ProductVariantCreateContent, {
ProductVariantCreateContentProps
@ -73,17 +75,17 @@ function canHitNext(
export interface ProductVariantCreateDialogProps
extends Omit<
ProductVariantCreateContentProps,
"dispatchFormDataAction" | "step"
"data" | "dispatchFormDataAction" | "step"
> {
open: boolean;
onClose: () => undefined;
onSubmit: (data: ProductVariantCreateFormData) => void;
onClose: () => void;
onSubmit: (data: ProductVariantBulkCreateInput[]) => void;
}
const ProductVariantCreateDialog: React.FC<
ProductVariantCreateDialogProps
> = props => {
const { open, onClose, ...contentProps } = props;
const { open, onClose, onSubmit, ...contentProps } = props;
const classes = useStyles(props);
const [step, setStep] = React.useState<ProductVariantCreateStep>(
"attributes"
@ -167,6 +169,7 @@ const ProductVariantCreateDialog: React.FC<
color="primary"
disabled={!canHitNext(step, data)}
variant="contained"
onClick={() => onSubmit(data.variants)}
>
<FormattedMessage
defaultMessage="Create"

View file

@ -63,12 +63,24 @@ const ProductVariantCreatePrices: React.FC<
const priceAttributeValues = data.price.all
? null
: data.price.attribute
? attributes.find(attribute => attribute.id === data.price.attribute).values
? attributes
.find(attribute => attribute.id === data.price.attribute)
.values.filter(value =>
data.attributes
.find(attribute => attribute.id === data.price.attribute)
.values.includes(value.slug)
)
: [];
const stockAttributeValues = data.stock.all
? null
: data.stock.attribute
? attributes.find(attribute => attribute.id === data.stock.attribute).values
? attributes
.find(attribute => attribute.id === data.stock.attribute)
.values.filter(value =>
data.attributes
.find(attribute => attribute.id === data.stock.attribute)
.values.includes(value.slug)
)
: [];
return (
@ -158,7 +170,7 @@ const ProductVariantCreatePrices: React.FC<
value={data.price.values[attributeValueIndex].value}
onChange={event =>
onAttributeValueChange(
attributeValue.id,
attributeValue.slug,
event.target.value,
"price"
)
@ -256,7 +268,7 @@ const ProductVariantCreatePrices: React.FC<
value={data.stock.values[attributeValueIndex].value}
onChange={event =>
onAttributeValueChange(
attributeValue.id,
attributeValue.slug,
event.target.value,
"stock"
)

View file

@ -4,11 +4,6 @@ import green from "@material-ui/core/colors/green";
import purple from "@material-ui/core/colors/purple";
import yellow from "@material-ui/core/colors/yellow";
import { Theme } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/styles";
@ -17,7 +12,9 @@ import React from "react";
import { FormattedMessage } from "react-intl";
import Hr from "@saleor/components/Hr";
import { ProductVariantCreateInput } from "@saleor/types/globalTypes";
import { maybe } from "@saleor/misc";
import { ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors } from "@saleor/products/types/ProductVariantBulkCreate";
import { ProductVariantBulkCreateInput } from "@saleor/types/globalTypes";
import { ProductDetails_product_productType_variantAttributes } from "../../types/ProductDetails";
import { ProductVariantCreateFormData } from "./form";
import { VariantField } from "./reducer";
@ -26,6 +23,7 @@ export interface ProductVariantCreateSummaryProps {
attributes: ProductDetails_product_productType_variantAttributes[];
currencySymbol: string;
data: ProductVariantCreateFormData;
errors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[];
onVariantDataChange: (
variantIndex: number,
field: VariantField,
@ -35,42 +33,58 @@ export interface ProductVariantCreateSummaryProps {
const colors = [blue, cyan, green, purple, yellow].map(color => color[800]);
const useStyles = makeStyles((theme: Theme) => ({
attributeValue: {
display: "inline-block",
marginRight: theme.spacing.unit
},
col: {
paddingLeft: theme.spacing.unit,
paddingRight: theme.spacing.unit
},
colName: {
paddingLeft: "0 !important",
width: "auto"
},
colPrice: {
width: 200
},
colSku: {
width: 210
},
colStock: {
width: 120
},
hr: {
marginBottom: theme.spacing.unit,
marginTop: theme.spacing.unit / 2
},
input: {
"& input": {
padding: "16px 12px 17px"
const useStyles = makeStyles(
(theme: Theme) => ({
attributeValue: {
display: "inline-block",
marginRight: theme.spacing.unit
},
marginTop: theme.spacing.unit / 2
col: {
...theme.typography.body2,
fontSize: 14,
paddingLeft: theme.spacing.unit,
paddingRight: theme.spacing.unit
},
colHeader: {
...theme.typography.body2,
fontSize: 14
},
colName: {
"&&": {
paddingLeft: "0 !important"
},
"&:not($colHeader)": {
paddingTop: theme.spacing.unit * 2
}
},
colPrice: {},
colSku: {},
colStock: {},
errorRow: {},
hr: {
marginBottom: theme.spacing.unit,
marginTop: theme.spacing.unit / 2
},
input: {
"& input": {
padding: "16px 12px 17px"
},
marginTop: theme.spacing.unit / 2
},
row: {
borderBottom: `1px solid ${theme.palette.divider}`,
display: "grid",
gridTemplateColumns: "1fr 200px 120px 210px",
padding: `${theme.spacing.unit}px 0`
}
}),
{
name: "ProductVariantCreateSummary"
}
}));
);
function getVariantName(
variant: ProductVariantCreateInput,
variant: ProductVariantBulkCreateInput,
attributes: ProductDetails_product_productType_variantAttributes[]
): string[] {
return attributes.reduce(
@ -78,7 +92,7 @@ function getVariantName(
...acc,
attribute.values.find(
value =>
value.id ===
value.slug ===
variant.attributes.find(
variantAttribute => variantAttribute.id === attribute.id
).values[0]
@ -91,7 +105,13 @@ function getVariantName(
const ProductVariantCreateSummary: React.FC<
ProductVariantCreateSummaryProps
> = props => {
const { attributes, currencySymbol, data, onVariantDataChange } = props;
const {
attributes,
currencySymbol,
data,
errors,
onVariantDataChange
} = props;
const classes = useStyles(props);
return (
@ -103,40 +123,69 @@ const ProductVariantCreateSummary: React.FC<
/>
</Typography>
<Hr className={classes.hr} />
<Table>
<TableHead>
<TableRow>
<TableCell className={classNames(classes.col, classes.colName)}>
<FormattedMessage
defaultMessage="Variant"
description="variant name"
/>
</TableCell>
<TableCell className={classNames(classes.col, classes.colStock)}>
<FormattedMessage
defaultMessage="Inventory"
description="variant stock amount"
/>
</TableCell>
<TableCell className={classNames(classes.col, classes.colPrice)}>
<FormattedMessage
defaultMessage="Price"
description="variant price"
/>
</TableCell>
<TableCell className={classNames(classes.col, classes.colSku)}>
<FormattedMessage defaultMessage="SKU" />
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data.variants.map((variant, variantIndex) => (
<TableRow
<div>
<div className={classes.row}>
<div
className={classNames(
classes.col,
classes.colHeader,
classes.colName
)}
>
<FormattedMessage
defaultMessage="Variant"
description="variant name"
/>
</div>
<div
className={classNames(
classes.col,
classes.colHeader,
classes.colPrice
)}
>
<FormattedMessage
defaultMessage="Price"
description="variant price"
/>
</div>
<div
className={classNames(
classes.col,
classes.colHeader,
classes.colStock
)}
>
<FormattedMessage
defaultMessage="Inventory"
description="variant stock amount"
/>
</div>
<div
className={classNames(
classes.col,
classes.colHeader,
classes.colSku
)}
>
<FormattedMessage defaultMessage="SKU" />
</div>
</div>
{data.variants.map((variant, variantIndex) => {
const variantErrors = errors.filter(
error => error.index === variantIndex
);
return (
<div
className={classNames(classes.row, {
[classes.errorRow]: variantErrors.length > 0
})}
key={variant.attributes
.map(attribute => attribute.values[0])
.join(":")}
>
<TableCell className={classNames(classes.col, classes.colName)}>
<div className={classNames(classes.col, classes.colName)}>
{getVariantName(variant, attributes).map(
(value, valueIndex) => (
<span
@ -149,31 +198,24 @@ const ProductVariantCreateSummary: React.FC<
</span>
)
)}
</TableCell>
<TableCell className={classNames(classes.col, classes.colStock)}>
<TextField
className={classes.input}
inputProps={{
min: 0,
type: "number"
}}
fullWidth
value={variant.quantity}
onChange={event =>
onVariantDataChange(
variantIndex,
"stock",
event.target.value
)
}
/>
</TableCell>
<TableCell className={classNames(classes.col, classes.colPrice)}>
</div>
<div className={classNames(classes.col, classes.colPrice)}>
<TextField
InputProps={{
endAdornment: currencySymbol
}}
className={classes.input}
error={
!!variantErrors.find(
error => error.field === "priceOverride"
)
}
helperText={maybe(
() =>
variantErrors.find(
error => error.field === "priceOverride"
).message
)}
inputProps={{
min: 0,
type: "number"
@ -188,21 +230,52 @@ const ProductVariantCreateSummary: React.FC<
)
}
/>
</TableCell>
<TableCell className={classNames(classes.col, classes.colSku)}>
</div>
<div className={classNames(classes.col, classes.colStock)}>
<TextField
className={classes.input}
error={
!!variantErrors.find(error => error.field === "quantity")
}
helperText={maybe(
() =>
variantErrors.find(error => error.field === "quantity")
.message
)}
inputProps={{
min: 0,
type: "number"
}}
fullWidth
value={variant.quantity}
onChange={event =>
onVariantDataChange(
variantIndex,
"stock",
event.target.value
)
}
/>
</div>
<div className={classNames(classes.col, classes.colSku)}>
<TextField
className={classes.input}
error={!!variantErrors.find(error => error.field === "sku")}
helperText={maybe(
() =>
variantErrors.find(error => error.field === "sku").message
)}
fullWidth
value={variant.sku}
onChange={event =>
onVariantDataChange(variantIndex, "sku", event.target.value)
}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
})}
</div>
</>
);
};

View file

@ -48,15 +48,15 @@ const ProductVariantCreateValues: React.FC<
{attribute.values.map(value => (
<ControlledCheckbox
checked={isSelected(
value.id,
value.slug,
data.attributes.find(
dataAttribute => attribute.id === dataAttribute.id
).values,
(a, b) => a === b
)}
name={`value:${value.id}`}
name={`value:${value.slug}`}
label={value.name}
onChange={() => onValueClick(attribute.id, value.id)}
onChange={() => onValueClick(attribute.id, value.slug)}
/>
))}
</div>

View file

@ -133,7 +133,6 @@ Object {
},
],
"priceOverride": "45.99",
"product": "",
"quantity": NaN,
"sku": "",
},
@ -159,7 +158,6 @@ Object {
},
],
"priceOverride": "45.99",
"product": "",
"quantity": NaN,
"sku": "",
},
@ -185,7 +183,6 @@ Object {
},
],
"priceOverride": "45.99",
"product": "",
"quantity": NaN,
"sku": "",
},
@ -211,7 +208,6 @@ Object {
},
],
"priceOverride": "45.99",
"product": "",
"quantity": NaN,
"sku": "",
},
@ -237,7 +233,6 @@ Object {
},
],
"priceOverride": "45.99",
"product": "",
"quantity": NaN,
"sku": "",
},
@ -263,7 +258,6 @@ Object {
},
],
"priceOverride": "45.99",
"product": "",
"quantity": NaN,
"sku": "",
},
@ -289,7 +283,6 @@ Object {
},
],
"priceOverride": "45.99",
"product": "",
"quantity": NaN,
"sku": "",
},
@ -315,7 +308,6 @@ Object {
},
],
"priceOverride": "45.99",
"product": "",
"quantity": NaN,
"sku": "",
},
@ -354,11 +346,11 @@ Object {
"value": "",
"values": Array [
Object {
"id": "val-1-1",
"slug": "val-1-1",
"value": "45.99",
},
Object {
"id": "val-1-7",
"slug": "val-1-7",
"value": "51.99",
},
],
@ -392,7 +384,6 @@ Object {
},
],
"priceOverride": "45.99",
"product": "",
"quantity": NaN,
"sku": "",
},
@ -418,7 +409,6 @@ Object {
},
],
"priceOverride": "45.99",
"product": "",
"quantity": NaN,
"sku": "",
},
@ -444,7 +434,6 @@ Object {
},
],
"priceOverride": "45.99",
"product": "",
"quantity": NaN,
"sku": "",
},
@ -470,7 +459,6 @@ Object {
},
],
"priceOverride": "45.99",
"product": "",
"quantity": NaN,
"sku": "",
},
@ -496,7 +484,6 @@ Object {
},
],
"priceOverride": "51.99",
"product": "",
"quantity": NaN,
"sku": "",
},
@ -522,7 +509,6 @@ Object {
},
],
"priceOverride": "51.99",
"product": "",
"quantity": NaN,
"sku": "",
},
@ -548,7 +534,6 @@ Object {
},
],
"priceOverride": "51.99",
"product": "",
"quantity": NaN,
"sku": "",
},
@ -574,7 +559,6 @@ Object {
},
],
"priceOverride": "51.99",
"product": "",
"quantity": NaN,
"sku": "",
},
@ -642,7 +626,6 @@ Object {
},
],
"priceOverride": "",
"product": "",
"quantity": 45,
"sku": "",
},
@ -668,7 +651,6 @@ Object {
},
],
"priceOverride": "",
"product": "",
"quantity": 45,
"sku": "",
},
@ -694,7 +676,6 @@ Object {
},
],
"priceOverride": "",
"product": "",
"quantity": 45,
"sku": "",
},
@ -720,7 +701,6 @@ Object {
},
],
"priceOverride": "",
"product": "",
"quantity": 45,
"sku": "",
},
@ -746,7 +726,6 @@ Object {
},
],
"priceOverride": "",
"product": "",
"quantity": 45,
"sku": "",
},
@ -772,7 +751,6 @@ Object {
},
],
"priceOverride": "",
"product": "",
"quantity": 45,
"sku": "",
},
@ -798,7 +776,6 @@ Object {
},
],
"priceOverride": "",
"product": "",
"quantity": 45,
"sku": "",
},
@ -824,7 +801,6 @@ Object {
},
],
"priceOverride": "",
"product": "",
"quantity": 45,
"sku": "",
},
@ -869,11 +845,11 @@ Object {
"value": "",
"values": Array [
Object {
"id": "val-1-1",
"slug": "val-1-1",
"value": "13",
},
Object {
"id": "val-1-7",
"slug": "val-1-7",
"value": "19",
},
],
@ -901,7 +877,6 @@ Object {
},
],
"priceOverride": "",
"product": "",
"quantity": 13,
"sku": "",
},
@ -927,7 +902,6 @@ Object {
},
],
"priceOverride": "",
"product": "",
"quantity": 13,
"sku": "",
},
@ -953,7 +927,6 @@ Object {
},
],
"priceOverride": "",
"product": "",
"quantity": 13,
"sku": "",
},
@ -979,7 +952,6 @@ Object {
},
],
"priceOverride": "",
"product": "",
"quantity": 13,
"sku": "",
},
@ -1005,7 +977,6 @@ Object {
},
],
"priceOverride": "",
"product": "",
"quantity": 19,
"sku": "",
},
@ -1031,7 +1002,6 @@ Object {
},
],
"priceOverride": "",
"product": "",
"quantity": 19,
"sku": "",
},
@ -1057,7 +1027,6 @@ Object {
},
],
"priceOverride": "",
"product": "",
"quantity": 19,
"sku": "",
},
@ -1083,7 +1052,6 @@ Object {
},
],
"priceOverride": "",
"product": "",
"quantity": 19,
"sku": "",
},

View file

@ -62,7 +62,7 @@ describe("Creates variant matrix", () => {
all: false,
attribute: attribute.id,
values: attribute.values.map((attributeValue, attributeValueIndex) => ({
id: attributeValue,
slug: attributeValue,
value: (price * (attributeValueIndex + 1)).toString()
}))
},
@ -120,7 +120,7 @@ describe("Creates variant matrix", () => {
all: false,
attribute: attribute.id,
values: attribute.values.map((attributeValue, attributeValueIndex) => ({
id: attributeValue,
slug: attributeValue,
value: (stock * (attributeValueIndex + 1)).toString()
}))
}
@ -166,7 +166,7 @@ describe("Creates variant matrix", () => {
all: false,
attribute: attribute.id,
values: attribute.values.map((attributeValue, attributeValueIndex) => ({
id: attributeValue,
slug: attributeValue,
value: (price * (attributeValueIndex + 1)).toString()
}))
},
@ -175,7 +175,7 @@ describe("Creates variant matrix", () => {
all: false,
attribute: attribute.id,
values: attribute.values.map((attributeValue, attributeValueIndex) => ({
id: attributeValue,
slug: attributeValue,
value: (stock * (attributeValueIndex + 1)).toString()
}))
}

View file

@ -1,4 +1,4 @@
import { ProductVariantCreateInput } from "@saleor/types/globalTypes";
import { ProductVariantBulkCreateInput } from "@saleor/types/globalTypes";
import {
AllOrAttribute,
Attribute,
@ -7,7 +7,7 @@ import {
interface CreateVariantAttributeValueInput {
attributeId: string;
attributeValueId: string;
attributeValueSlug: string;
}
type CreateVariantInput = CreateVariantAttributeValueInput[];
@ -20,7 +20,7 @@ function getAttributeValuePriceOrStock(
);
const attributeValue = priceOrStock.values.find(
attributeValue => attribute.attributeValueId === attributeValue.id
attributeValue => attribute.attributeValueSlug === attributeValue.slug
);
return attributeValue.value;
@ -29,7 +29,7 @@ function getAttributeValuePriceOrStock(
function createVariant(
data: ProductVariantCreateFormData,
attributes: CreateVariantInput
): ProductVariantCreateInput {
): ProductVariantBulkCreateInput {
const priceOverride = data.price.all
? data.price.value
: getAttributeValuePriceOrStock(attributes, data.price);
@ -43,10 +43,9 @@ function createVariant(
return {
attributes: attributes.map(attribute => ({
id: attribute.attributeId,
values: [attribute.attributeValueId]
values: [attribute.attributeValueSlug]
})),
priceOverride,
product: "",
quantity,
sku: ""
};
@ -56,11 +55,11 @@ function addAttributeToVariant(
attribute: Attribute,
variant: CreateVariantInput
): CreateVariantInput[] {
return attribute.values.map(attributeValueId => [
return attribute.values.map(attributeValueSlug => [
...variant,
{
attributeId: attribute.id,
attributeValueId
attributeValueSlug
}
]);
}
@ -91,7 +90,7 @@ export function createVariantFlatMatrixDimension(
export function createVariants(
data: ProductVariantCreateFormData
): ProductVariantCreateInput[] {
): ProductVariantBulkCreateInput[] {
if (
(!data.price.all && !data.price.attribute) ||
(!data.stock.all && !data.stock.attribute)

View file

@ -74,11 +74,11 @@ const price: AllOrAttribute = {
value: "",
values: [
{
id: thirdStep.attributes[1].values[0],
slug: thirdStep.attributes[1].values[0],
value: "24.99"
},
{
id: thirdStep.attributes[1].values[1],
slug: thirdStep.attributes[1].values[1],
value: "26.99"
}
]
@ -89,11 +89,11 @@ const stock: AllOrAttribute = {
value: "",
values: [
{
id: thirdStep.attributes[2].values[0],
slug: thirdStep.attributes[2].values[0],
value: "50"
},
{
id: thirdStep.attributes[2].values[1],
slug: thirdStep.attributes[2].values[1],
value: "35"
}
]

View file

@ -1,7 +1,7 @@
import { ProductVariantCreateInput } from "../../../types/globalTypes";
import { ProductVariantBulkCreateInput } from "../../../types/globalTypes";
export interface AttributeValue {
id: string;
slug: string;
value: string;
}
export interface AllOrAttribute {
@ -18,7 +18,7 @@ export interface ProductVariantCreateFormData {
attributes: Attribute[];
price: AllOrAttribute;
stock: AllOrAttribute;
variants: ProductVariantCreateInput[];
variants: ProductVariantBulkCreateInput[];
}
export const initialForm: ProductVariantCreateFormData = {

View file

@ -108,20 +108,20 @@ function applyStockToAll(
function changeAttributeValuePrice(
state: ProductVariantCreateFormData,
attributeValueId: string,
attributeValueSlug: string,
price: string
): ProductVariantCreateFormData {
const index = state.price.values.findIndex(
value => value.id === attributeValueId
value => value.slug === attributeValueSlug
);
if (index === -1) {
throw new Error(`Value with id ${attributeValueId} not found`);
throw new Error(`Value with id ${attributeValueSlug} not found`);
}
const values = updateAtIndex(
{
id: attributeValueId,
slug: attributeValueSlug,
value: price
},
state.price.values,
@ -144,20 +144,20 @@ function changeAttributeValuePrice(
function changeAttributeValueStock(
state: ProductVariantCreateFormData,
attributeValueId: string,
attributeValueSlug: string,
stock: string
): ProductVariantCreateFormData {
const index = state.stock.values.findIndex(
value => value.id === attributeValueId
value => value.slug === attributeValueSlug
);
if (index === -1) {
throw new Error(`Value with id ${attributeValueId} not found`);
throw new Error(`Value with id ${attributeValueSlug} not found`);
}
const values = updateAtIndex(
{
id: attributeValueId,
slug: attributeValueSlug,
value: stock
},
state.stock.values,
@ -185,8 +185,8 @@ function changeApplyPriceToAttributeId(
const attribute = state.attributes.find(
attribute => attribute.id === attributeId
);
const values = attribute.values.map(id => ({
id,
const values = attribute.values.map(slug => ({
slug,
value: ""
}));
const data = {
@ -211,8 +211,8 @@ function changeApplyStockToAttributeId(
const attribute = state.attributes.find(
attribute => attribute.id === attributeId
);
const values = attribute.values.map(id => ({
id,
const values = attribute.values.map(slug => ({
slug,
value: ""
}));

View file

@ -69,6 +69,7 @@ interface ProductVariantsProps extends ListActions, WithStyles<typeof styles> {
fallbackPrice?: ProductVariant_costPrice;
onRowClick: (id: string) => () => void;
onVariantAdd?();
onVariantsAdd?();
}
const numberOfColumns = 5;
@ -81,6 +82,7 @@ export const ProductVariants = withStyles(styles, { name: "ProductVariants" })(
fallbackPrice,
onRowClick,
onVariantAdd,
onVariantsAdd,
isChecked,
selected,
toggle,
@ -98,7 +100,7 @@ export const ProductVariants = withStyles(styles, { name: "ProductVariants" })(
description: "section header"
})}
toolbar={
<>
hasVariants ? (
<Button
onClick={onVariantAdd}
variant="text"
@ -110,7 +112,19 @@ export const ProductVariants = withStyles(styles, { name: "ProductVariants" })(
description="button"
/>
</Button>
</>
) : (
<Button
onClick={onVariantsAdd}
variant="text"
color="primary"
data-tc="button-add-variants"
>
<FormattedMessage
defaultMessage="Create variants"
description="button"
/>
</Button>
)
}
/>
{!variants.length && (

View file

@ -7,6 +7,7 @@ import {
TypedProductImageCreateMutation,
TypedProductImageDeleteMutation,
TypedProductUpdateMutation,
TypedProductVariantBulkCreateMutation,
TypedProductVariantBulkDeleteMutation,
TypedSimpleProductUpdateMutation
} from "../mutations";
@ -25,6 +26,10 @@ import {
ProductImageReorderVariables
} from "../types/ProductImageReorder";
import { ProductUpdate, ProductUpdateVariables } from "../types/ProductUpdate";
import {
ProductVariantBulkCreate,
ProductVariantBulkCreateVariables
} from "../types/ProductVariantBulkCreate";
import {
ProductVariantBulkDelete,
ProductVariantBulkDeleteVariables
@ -38,6 +43,10 @@ import ProductImagesReorderProvider from "./ProductImagesReorder";
interface ProductUpdateOperationsProps {
product: ProductDetails_product;
children: (props: {
bulkProductVariantCreate: PartialMutationProviderOutput<
ProductVariantBulkCreate,
ProductVariantBulkCreateVariables
>;
bulkProductVariantDelete: PartialMutationProviderOutput<
ProductVariantBulkDelete,
ProductVariantBulkDeleteVariables
@ -67,6 +76,7 @@ interface ProductUpdateOperationsProps {
SimpleProductUpdateVariables
>;
}) => React.ReactNode;
onBulkProductVariantCreate?: (data: ProductVariantBulkCreate) => void;
onBulkProductVariantDelete?: (data: ProductVariantBulkDelete) => void;
onDelete?: (data: ProductDelete) => void;
onImageCreate?: (data: ProductImageCreate) => void;
@ -80,6 +90,7 @@ const ProductUpdateOperations: React.StatelessComponent<
> = ({
product,
children,
onBulkProductVariantCreate,
onBulkProductVariantDelete,
onDelete,
onImageDelete,
@ -112,31 +123,40 @@ const ProductUpdateOperations: React.StatelessComponent<
<TypedProductVariantBulkDeleteMutation
onCompleted={onBulkProductVariantDelete}
>
{(...bulkProductVariantDelete) =>
children({
bulkProductVariantDelete: getMutationProviderData(
...bulkProductVariantDelete
),
createProductImage: getMutationProviderData(
...createProductImage
),
deleteProduct: getMutationProviderData(
...deleteProduct
),
deleteProductImage: getMutationProviderData(
...deleteProductImage
),
reorderProductImages: getMutationProviderData(
...reorderProductImages
),
updateProduct: getMutationProviderData(
...updateProduct
),
updateSimpleProduct: getMutationProviderData(
...updateSimpleProduct
)
})
}
{(...bulkProductVariantDelete) => (
<TypedProductVariantBulkCreateMutation
onCompleted={onBulkProductVariantCreate}
>
{(...bulkProductVariantCreate) =>
children({
bulkProductVariantCreate: getMutationProviderData(
...bulkProductVariantCreate
),
bulkProductVariantDelete: getMutationProviderData(
...bulkProductVariantDelete
),
createProductImage: getMutationProviderData(
...createProductImage
),
deleteProduct: getMutationProviderData(
...deleteProduct
),
deleteProductImage: getMutationProviderData(
...deleteProductImage
),
reorderProductImages: getMutationProviderData(
...reorderProductImages
),
updateProduct: getMutationProviderData(
...updateProduct
),
updateSimpleProduct: getMutationProviderData(
...updateSimpleProduct
)
})
}
</TypedProductVariantBulkCreateMutation>
)}
</TypedProductVariantBulkDeleteMutation>
)}
</TypedSimpleProductUpdateMutation>

View file

@ -45,6 +45,10 @@ import {
productBulkPublish,
productBulkPublishVariables
} from "./types/productBulkPublish";
import {
ProductVariantBulkCreate,
ProductVariantBulkCreateVariables
} from "./types/ProductVariantBulkCreate";
import {
ProductVariantBulkDelete,
ProductVariantBulkDeleteVariables
@ -440,6 +444,30 @@ export const TypedProductBulkPublishMutation = TypedMutation<
productBulkPublishVariables
>(productBulkPublishMutation);
export const ProductVariantBulkCreateMutation = gql`
mutation ProductVariantBulkCreate(
$id: ID!
$inputs: [ProductVariantBulkCreateInput]!
) {
productVariantBulkCreate(product: $id, variants: $inputs) {
bulkProductErrors {
field
message
code
index
}
errors {
field
message
}
}
}
`;
export const TypedProductVariantBulkCreateMutation = TypedMutation<
ProductVariantBulkCreate,
ProductVariantBulkCreateVariables
>(ProductVariantBulkCreateMutation);
export const ProductVariantBulkDeleteMutation = gql`
mutation ProductVariantBulkDelete($ids: [ID!]!) {
productVariantBulkDelete(ids: $ids) {

View file

@ -267,6 +267,7 @@ const productDetailsQuery = gql`
values {
id
name
slug
}
}
}

View file

@ -143,6 +143,7 @@ export interface ProductDetails_product_productType_variantAttributes_values {
__typename: "AttributeValue";
id: string;
name: string | null;
slug: string | null;
}
export interface ProductDetails_product_productType_variantAttributes {

View file

@ -0,0 +1,38 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ProductVariantBulkCreateInput, ProductErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: ProductVariantBulkCreate
// ====================================================
export interface ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors {
__typename: "BulkProductError";
field: string | null;
message: string | null;
code: ProductErrorCode | null;
index: number | null;
}
export interface ProductVariantBulkCreate_productVariantBulkCreate_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface ProductVariantBulkCreate_productVariantBulkCreate {
__typename: "ProductVariantBulkCreate";
bulkProductErrors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[] | null;
errors: ProductVariantBulkCreate_productVariantBulkCreate_errors[] | null;
}
export interface ProductVariantBulkCreate {
productVariantBulkCreate: ProductVariantBulkCreate_productVariantBulkCreate | null;
}
export interface ProductVariantBulkCreateVariables {
id: string;
inputs: (ProductVariantBulkCreateInput | null)[];
}

View file

@ -53,7 +53,7 @@ export const productListUrl = (params?: ProductListUrlQueryParams): string =>
export const productPath = (id: string) => urlJoin(productSection + id);
export type ProductUrlDialog = "remove";
export type ProductUrlQueryParams = BulkAction &
Dialog<"remove" | "remove-variants">;
Dialog<"create-variants" | "remove" | "remove-variants">;
export const productUrl = (id: string, params?: ProductUrlQueryParams) =>
productPath(encodeURIComponent(id)) + "?" + stringifyQs(params);

View file

@ -10,7 +10,10 @@ import { WindowTitle } from "@saleor/components/WindowTitle";
import useBulkActions from "@saleor/hooks/useBulkActions";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import useShop from "@saleor/hooks/useShop";
import { commonMessages } from "@saleor/intl";
import ProductVariantCreateDialog from "@saleor/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog";
import { ProductVariantBulkCreate } from "@saleor/products/types/ProductVariantBulkCreate";
import { DEFAULT_INITIAL_SEARCH_DATA } from "../../../config";
import SearchCategories from "../../../containers/SearchCategories";
import SearchCollections from "../../../containers/SearchCollections";
@ -54,6 +57,7 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
params.ids
);
const intl = useIntl();
const shop = useShop();
const openModal = (action: ProductUrlDialog) =>
navigate(
@ -115,6 +119,15 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
const handleVariantAdd = () =>
navigate(productVariantAddUrl(id));
const handleBulkProductVariantCreate = (
data: ProductVariantBulkCreate
) => {
if (data.productVariantBulkCreate.errors.length === 0) {
navigate(productUrl(id), true);
refetch();
}
};
const handleBulkProductVariantDelete = (
data: ProductVariantBulkDelete
) => {
@ -125,10 +138,19 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
}
};
const handleVariantCreatorOpen = () =>
navigate(
productUrl(id, {
...params,
action: "create-variants"
})
);
const product = data ? data.product : undefined;
return (
<ProductUpdateOperations
product={product}
onBulkProductVariantCreate={handleBulkProductVariantCreate}
onBulkProductVariantDelete={handleBulkProductVariantDelete}
onDelete={handleDelete}
onImageCreate={handleImageCreate}
@ -136,6 +158,7 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
onUpdate={handleUpdate}
>
{({
bulkProductVariantCreate,
bulkProductVariantDelete,
createProductImage,
deleteProduct,
@ -245,6 +268,7 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
onImageReorder={handleImageReorder}
onSubmit={handleSubmit}
onVariantAdd={handleVariantAdd}
onVariantsAdd={handleVariantCreatorOpen}
onVariantShow={variantId => () =>
navigate(
productVariantEditUrl(product.id, variantId)
@ -328,6 +352,34 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
/>
</DialogContentText>
</ActionDialog>
<ProductVariantCreateDialog
errors={maybe(
() =>
bulkProductVariantCreate.opts.data
.productVariantBulkCreate.bulkProductErrors,
[]
)}
open={params.action === "create-variants"}
attributes={maybe(
() => data.product.productType.variantAttributes,
[]
)}
currencySymbol={maybe(() => shop.defaultCurrency)}
onClose={() =>
navigate(
productUrl(id, {
...params,
action: undefined
})
)
}
onSubmit={inputs =>
bulkProductVariantCreate.mutate({
id,
inputs
})
}
/>
</>
);
}}

View file

@ -58,18 +58,20 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
) =>
variantCreate({
variables: {
attributes: formData.attributes
.filter(attribute => attribute.value !== "")
.map(attribute => ({
id: attribute.id,
values: [attribute.value]
})),
costPrice: decimal(formData.costPrice),
priceOverride: decimal(formData.priceOverride),
product: productId,
quantity: formData.quantity || null,
sku: formData.sku,
trackInventory: true
input: {
attributes: formData.attributes
.filter(attribute => attribute.value !== "")
.map(attribute => ({
id: attribute.id,
values: [attribute.value]
})),
costPrice: decimal(formData.costPrice),
priceOverride: decimal(formData.priceOverride),
product: productId,
quantity: formData.quantity || null,
sku: formData.sku,
trackInventory: true
}
}
});
const handleVariantClick = (id: string) =>

File diff suppressed because it is too large Load diff

View file

@ -30,6 +30,7 @@ const props: ProductUpdatePageProps = {
onSubmit: () => undefined,
onVariantAdd: () => undefined,
onVariantShow: () => undefined,
onVariantsAdd: () => undefined,
placeholderImage,
product,
saveButtonBarState: "default",

View file

@ -193,6 +193,20 @@ export enum PermissionEnum {
MANAGE_WEBHOOKS = "MANAGE_WEBHOOKS",
}
export enum ProductErrorCode {
ALREADY_EXISTS = "ALREADY_EXISTS",
ATTRIBUTE_ALREADY_ASSIGNED = "ATTRIBUTE_ALREADY_ASSIGNED",
ATTRIBUTE_CANNOT_BE_ASSIGNED = "ATTRIBUTE_CANNOT_BE_ASSIGNED",
ATTRIBUTE_VARIANTS_DISABLED = "ATTRIBUTE_VARIANTS_DISABLED",
GRAPHQL_ERROR = "GRAPHQL_ERROR",
INVALID = "INVALID",
NOT_FOUND = "NOT_FOUND",
NOT_PRODUCTS_IMAGE = "NOT_PRODUCTS_IMAGE",
REQUIRED = "REQUIRED",
UNIQUE = "UNIQUE",
VARIANT_NO_DIGITAL_CONTENT = "VARIANT_NO_DIGITAL_CONTENT",
}
export enum ProductOrderField {
DATE = "DATE",
MINIMAL_PRICE = "MINIMAL_PRICE",
@ -614,6 +628,16 @@ export interface ProductTypeInput {
taxCode?: string | null;
}
export interface ProductVariantBulkCreateInput {
attributes: (AttributeValueInput | null)[];
costPrice?: any | null;
priceOverride?: any | null;
sku: string;
quantity?: number | null;
trackInventory?: boolean | null;
weight?: any | null;
}
export interface ProductVariantCreateInput {
attributes: (AttributeValueInput | null)[];
costPrice?: any | null;