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

View file

@ -4,6 +4,8 @@ import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
import { attributes } from "@saleor/attributes/fixtures"; 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 Decorator from "../../../storybook/Decorator";
import { createVariants } from "./createVariants"; import { createVariants } from "./createVariants";
import { AllOrAttribute } from "./form"; import { AllOrAttribute } from "./form";
@ -19,7 +21,7 @@ const price: AllOrAttribute = {
attribute: selectedAttributes[1].id, attribute: selectedAttributes[1].id,
value: "2.79", value: "2.79",
values: selectedAttributes[1].values.map((attribute, attributeIndex) => ({ values: selectedAttributes[1].values.map((attribute, attributeIndex) => ({
id: attribute.id, slug: attribute.slug,
value: (attributeIndex + 4).toFixed(2) value: (attributeIndex + 4).toFixed(2)
})) }))
}; };
@ -29,7 +31,7 @@ const stock: AllOrAttribute = {
attribute: selectedAttributes[1].id, attribute: selectedAttributes[1].id,
value: "8", value: "8",
values: selectedAttributes[1].values.map((attribute, attributeIndex) => ({ values: selectedAttributes[1].values.map((attribute, attributeIndex) => ({
id: attribute.id, slug: attribute.slug,
value: (selectedAttributes.length * 10 - attributeIndex).toString() value: (selectedAttributes.length * 10 - attributeIndex).toString()
})) }))
}; };
@ -37,10 +39,20 @@ const stock: AllOrAttribute = {
const dataAttributes = selectedAttributes.map(attribute => ({ const dataAttributes = selectedAttributes.map(attribute => ({
id: attribute.id, id: attribute.id,
values: attribute.values values: attribute.values
.map(value => value.id) .map(value => value.slug)
.filter((_, valueIndex) => valueIndex % 2 !== 1) .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 = { const props: ProductVariantCreateContentProps = {
attributes, attributes,
currencySymbol: "USD", currencySymbol: "USD",
@ -56,6 +68,7 @@ const props: ProductVariantCreateContentProps = {
}) })
}, },
dispatchFormDataAction: () => undefined, dispatchFormDataAction: () => undefined,
errors: [],
step: "attributes" step: "attributes"
}; };
@ -78,9 +91,26 @@ storiesOf("Views / Products / Create multiple variants", module)
)) ))
.add("prices and SKU", () => ( .add("prices and SKU", () => (
<ProductVariantCreateContent {...props} step="prices" /> <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" /> <ProductVariantCreateContent {...props} step="summary" />
))
.add("errors", () => (
<ProductVariantCreateContent {...props} step="summary" errors={errors} />
)); ));
storiesOf("Views / Products / Create multiple variants", module) 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 Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody"; import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell"; import TableCell from "@material-ui/core/TableCell";
@ -18,14 +17,14 @@ export interface ProductVariantCreateAttributesProps {
onAttributeClick: (id: string) => void; onAttributeClick: (id: string) => void;
} }
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles({
checkboxCell: { checkboxCell: {
paddingLeft: 0 paddingLeft: 0
}, },
wideCell: { wideCell: {
width: "100%" width: "100%"
} }
})); });
const ProductVariantCreateAttributes: React.FC< const ProductVariantCreateAttributes: React.FC<
ProductVariantCreateAttributesProps ProductVariantCreateAttributesProps

View file

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

View file

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

View file

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

View file

@ -4,11 +4,6 @@ import green from "@material-ui/core/colors/green";
import purple from "@material-ui/core/colors/purple"; import purple from "@material-ui/core/colors/purple";
import yellow from "@material-ui/core/colors/yellow"; import yellow from "@material-ui/core/colors/yellow";
import { Theme } from "@material-ui/core/styles"; 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 TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/styles"; import { makeStyles } from "@material-ui/styles";
@ -17,7 +12,9 @@ import React from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import Hr from "@saleor/components/Hr"; 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 { ProductDetails_product_productType_variantAttributes } from "../../types/ProductDetails";
import { ProductVariantCreateFormData } from "./form"; import { ProductVariantCreateFormData } from "./form";
import { VariantField } from "./reducer"; import { VariantField } from "./reducer";
@ -26,6 +23,7 @@ export interface ProductVariantCreateSummaryProps {
attributes: ProductDetails_product_productType_variantAttributes[]; attributes: ProductDetails_product_productType_variantAttributes[];
currencySymbol: string; currencySymbol: string;
data: ProductVariantCreateFormData; data: ProductVariantCreateFormData;
errors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[];
onVariantDataChange: ( onVariantDataChange: (
variantIndex: number, variantIndex: number,
field: VariantField, field: VariantField,
@ -35,42 +33,58 @@ export interface ProductVariantCreateSummaryProps {
const colors = [blue, cyan, green, purple, yellow].map(color => color[800]); const colors = [blue, cyan, green, purple, yellow].map(color => color[800]);
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles(
attributeValue: { (theme: Theme) => ({
display: "inline-block", attributeValue: {
marginRight: theme.spacing.unit 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"
}, },
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( function getVariantName(
variant: ProductVariantCreateInput, variant: ProductVariantBulkCreateInput,
attributes: ProductDetails_product_productType_variantAttributes[] attributes: ProductDetails_product_productType_variantAttributes[]
): string[] { ): string[] {
return attributes.reduce( return attributes.reduce(
@ -78,7 +92,7 @@ function getVariantName(
...acc, ...acc,
attribute.values.find( attribute.values.find(
value => value =>
value.id === value.slug ===
variant.attributes.find( variant.attributes.find(
variantAttribute => variantAttribute.id === attribute.id variantAttribute => variantAttribute.id === attribute.id
).values[0] ).values[0]
@ -91,7 +105,13 @@ function getVariantName(
const ProductVariantCreateSummary: React.FC< const ProductVariantCreateSummary: React.FC<
ProductVariantCreateSummaryProps ProductVariantCreateSummaryProps
> = props => { > = props => {
const { attributes, currencySymbol, data, onVariantDataChange } = props; const {
attributes,
currencySymbol,
data,
errors,
onVariantDataChange
} = props;
const classes = useStyles(props); const classes = useStyles(props);
return ( return (
@ -103,40 +123,69 @@ const ProductVariantCreateSummary: React.FC<
/> />
</Typography> </Typography>
<Hr className={classes.hr} /> <Hr className={classes.hr} />
<Table> <div>
<TableHead> <div className={classes.row}>
<TableRow> <div
<TableCell className={classNames(classes.col, classes.colName)}> className={classNames(
<FormattedMessage classes.col,
defaultMessage="Variant" classes.colHeader,
description="variant name" classes.colName
/> )}
</TableCell> >
<TableCell className={classNames(classes.col, classes.colStock)}> <FormattedMessage
<FormattedMessage defaultMessage="Variant"
defaultMessage="Inventory" description="variant name"
description="variant stock amount" />
/> </div>
</TableCell> <div
<TableCell className={classNames(classes.col, classes.colPrice)}> className={classNames(
<FormattedMessage classes.col,
defaultMessage="Price" classes.colHeader,
description="variant price" classes.colPrice
/> )}
</TableCell> >
<TableCell className={classNames(classes.col, classes.colSku)}> <FormattedMessage
<FormattedMessage defaultMessage="SKU" /> defaultMessage="Price"
</TableCell> description="variant price"
</TableRow> />
</TableHead> </div>
<TableBody> <div
{data.variants.map((variant, variantIndex) => ( className={classNames(
<TableRow 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 key={variant.attributes
.map(attribute => attribute.values[0]) .map(attribute => attribute.values[0])
.join(":")} .join(":")}
> >
<TableCell className={classNames(classes.col, classes.colName)}> <div className={classNames(classes.col, classes.colName)}>
{getVariantName(variant, attributes).map( {getVariantName(variant, attributes).map(
(value, valueIndex) => ( (value, valueIndex) => (
<span <span
@ -149,31 +198,24 @@ const ProductVariantCreateSummary: React.FC<
</span> </span>
) )
)} )}
</TableCell> </div>
<TableCell className={classNames(classes.col, classes.colStock)}> <div className={classNames(classes.col, classes.colPrice)}>
<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)}>
<TextField <TextField
InputProps={{ InputProps={{
endAdornment: currencySymbol endAdornment: currencySymbol
}} }}
className={classes.input} className={classes.input}
error={
!!variantErrors.find(
error => error.field === "priceOverride"
)
}
helperText={maybe(
() =>
variantErrors.find(
error => error.field === "priceOverride"
).message
)}
inputProps={{ inputProps={{
min: 0, min: 0,
type: "number" type: "number"
@ -188,21 +230,52 @@ const ProductVariantCreateSummary: React.FC<
) )
} }
/> />
</TableCell> </div>
<TableCell className={classNames(classes.col, classes.colSku)}> <div className={classNames(classes.col, classes.colStock)}>
<TextField <TextField
className={classes.input} 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 fullWidth
value={variant.sku} value={variant.sku}
onChange={event => onChange={event =>
onVariantDataChange(variantIndex, "sku", event.target.value) onVariantDataChange(variantIndex, "sku", event.target.value)
} }
/> />
</TableCell> </div>
</TableRow> </div>
))} );
</TableBody> })}
</Table> </div>
</> </>
); );
}; };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -45,6 +45,10 @@ import {
productBulkPublish, productBulkPublish,
productBulkPublishVariables productBulkPublishVariables
} from "./types/productBulkPublish"; } from "./types/productBulkPublish";
import {
ProductVariantBulkCreate,
ProductVariantBulkCreateVariables
} from "./types/ProductVariantBulkCreate";
import { import {
ProductVariantBulkDelete, ProductVariantBulkDelete,
ProductVariantBulkDeleteVariables ProductVariantBulkDeleteVariables
@ -440,6 +444,30 @@ export const TypedProductBulkPublishMutation = TypedMutation<
productBulkPublishVariables productBulkPublishVariables
>(productBulkPublishMutation); >(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` export const ProductVariantBulkDeleteMutation = gql`
mutation ProductVariantBulkDelete($ids: [ID!]!) { mutation ProductVariantBulkDelete($ids: [ID!]!) {
productVariantBulkDelete(ids: $ids) { productVariantBulkDelete(ids: $ids) {

View file

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

View file

@ -143,6 +143,7 @@ export interface ProductDetails_product_productType_variantAttributes_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
name: string | null; name: string | null;
slug: string | null;
} }
export interface ProductDetails_product_productType_variantAttributes { 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 const productPath = (id: string) => urlJoin(productSection + id);
export type ProductUrlDialog = "remove"; export type ProductUrlDialog = "remove";
export type ProductUrlQueryParams = BulkAction & export type ProductUrlQueryParams = BulkAction &
Dialog<"remove" | "remove-variants">; Dialog<"create-variants" | "remove" | "remove-variants">;
export const productUrl = (id: string, params?: ProductUrlQueryParams) => export const productUrl = (id: string, params?: ProductUrlQueryParams) =>
productPath(encodeURIComponent(id)) + "?" + stringifyQs(params); productPath(encodeURIComponent(id)) + "?" + stringifyQs(params);

View file

@ -10,7 +10,10 @@ import { WindowTitle } from "@saleor/components/WindowTitle";
import useBulkActions from "@saleor/hooks/useBulkActions"; import useBulkActions from "@saleor/hooks/useBulkActions";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import useShop from "@saleor/hooks/useShop";
import { commonMessages } from "@saleor/intl"; 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 { DEFAULT_INITIAL_SEARCH_DATA } from "../../../config";
import SearchCategories from "../../../containers/SearchCategories"; import SearchCategories from "../../../containers/SearchCategories";
import SearchCollections from "../../../containers/SearchCollections"; import SearchCollections from "../../../containers/SearchCollections";
@ -54,6 +57,7 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
params.ids params.ids
); );
const intl = useIntl(); const intl = useIntl();
const shop = useShop();
const openModal = (action: ProductUrlDialog) => const openModal = (action: ProductUrlDialog) =>
navigate( navigate(
@ -115,6 +119,15 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
const handleVariantAdd = () => const handleVariantAdd = () =>
navigate(productVariantAddUrl(id)); navigate(productVariantAddUrl(id));
const handleBulkProductVariantCreate = (
data: ProductVariantBulkCreate
) => {
if (data.productVariantBulkCreate.errors.length === 0) {
navigate(productUrl(id), true);
refetch();
}
};
const handleBulkProductVariantDelete = ( const handleBulkProductVariantDelete = (
data: ProductVariantBulkDelete 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; const product = data ? data.product : undefined;
return ( return (
<ProductUpdateOperations <ProductUpdateOperations
product={product} product={product}
onBulkProductVariantCreate={handleBulkProductVariantCreate}
onBulkProductVariantDelete={handleBulkProductVariantDelete} onBulkProductVariantDelete={handleBulkProductVariantDelete}
onDelete={handleDelete} onDelete={handleDelete}
onImageCreate={handleImageCreate} onImageCreate={handleImageCreate}
@ -136,6 +158,7 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
onUpdate={handleUpdate} onUpdate={handleUpdate}
> >
{({ {({
bulkProductVariantCreate,
bulkProductVariantDelete, bulkProductVariantDelete,
createProductImage, createProductImage,
deleteProduct, deleteProduct,
@ -245,6 +268,7 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
onImageReorder={handleImageReorder} onImageReorder={handleImageReorder}
onSubmit={handleSubmit} onSubmit={handleSubmit}
onVariantAdd={handleVariantAdd} onVariantAdd={handleVariantAdd}
onVariantsAdd={handleVariantCreatorOpen}
onVariantShow={variantId => () => onVariantShow={variantId => () =>
navigate( navigate(
productVariantEditUrl(product.id, variantId) productVariantEditUrl(product.id, variantId)
@ -328,6 +352,34 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
/> />
</DialogContentText> </DialogContentText>
</ActionDialog> </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({ variantCreate({
variables: { variables: {
attributes: formData.attributes input: {
.filter(attribute => attribute.value !== "") attributes: formData.attributes
.map(attribute => ({ .filter(attribute => attribute.value !== "")
id: attribute.id, .map(attribute => ({
values: [attribute.value] id: attribute.id,
})), values: [attribute.value]
costPrice: decimal(formData.costPrice), })),
priceOverride: decimal(formData.priceOverride), costPrice: decimal(formData.costPrice),
product: productId, priceOverride: decimal(formData.priceOverride),
quantity: formData.quantity || null, product: productId,
sku: formData.sku, quantity: formData.quantity || null,
trackInventory: true sku: formData.sku,
trackInventory: true
}
} }
}); });
const handleVariantClick = (id: string) => 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, onSubmit: () => undefined,
onVariantAdd: () => undefined, onVariantAdd: () => undefined,
onVariantShow: () => undefined, onVariantShow: () => undefined,
onVariantsAdd: () => undefined,
placeholderImage, placeholderImage,
product, product,
saveButtonBarState: "default", saveButtonBarState: "default",

View file

@ -193,6 +193,20 @@ export enum PermissionEnum {
MANAGE_WEBHOOKS = "MANAGE_WEBHOOKS", 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 { export enum ProductOrderField {
DATE = "DATE", DATE = "DATE",
MINIMAL_PRICE = "MINIMAL_PRICE", MINIMAL_PRICE = "MINIMAL_PRICE",
@ -614,6 +628,16 @@ export interface ProductTypeInput {
taxCode?: string | null; 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 { export interface ProductVariantCreateInput {
attributes: (AttributeValueInput | null)[]; attributes: (AttributeValueInput | null)[];
costPrice?: any | null; costPrice?: any | null;