This commit is contained in:
dominik-zeglen 2020-04-07 14:46:10 +02:00
parent 688fc83f55
commit 64371d04a6
14 changed files with 354 additions and 155 deletions

60
package-lock.json generated
View file

@ -16,56 +16,56 @@
} }
}, },
"@apollo/react-common": { "@apollo/react-common": {
"version": "3.1.3", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/@apollo/react-common/-/react-common-3.1.3.tgz", "resolved": "https://registry.npmjs.org/@apollo/react-common/-/react-common-3.1.4.tgz",
"integrity": "sha512-Q7ZjDOeqjJf/AOGxUMdGxKF+JVClRXrYBGVq+SuVFqANRpd68MxtVV2OjCWavsFAN0eqYnRqRUrl7vtUCiJqeg==", "integrity": "sha512-X5Kyro73bthWSCBJUC5XYQqMnG0dLWuDZmVkzog9dynovhfiVCV4kPSdgSIkqnb++cwCzOVuQ4rDKVwo2XRzQA==",
"requires": { "requires": {
"ts-invariant": "^0.4.4", "ts-invariant": "^0.4.4",
"tslib": "^1.10.0" "tslib": "^1.10.0"
} }
}, },
"@apollo/react-components": { "@apollo/react-components": {
"version": "3.1.3", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/@apollo/react-components/-/react-components-3.1.3.tgz", "resolved": "https://registry.npmjs.org/@apollo/react-components/-/react-components-3.1.5.tgz",
"integrity": "sha512-H0l2JKDQMz+LkM93QK7j3ThbNXkWQCduN3s3eKxFN3Rdg7rXsrikJWvx2wQ868jmqy0VhwJbS1vYdRLdh114uQ==", "integrity": "sha512-c82VyUuE9VBnJB7bnX+3dmwpIPMhyjMwyoSLyQWPHxz8jK4ak30XszJtqFf4eC4hwvvLYa+Ou6X73Q8V8e2/jg==",
"requires": { "requires": {
"@apollo/react-common": "^3.1.3", "@apollo/react-common": "^3.1.4",
"@apollo/react-hooks": "^3.1.3", "@apollo/react-hooks": "^3.1.5",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"ts-invariant": "^0.4.4", "ts-invariant": "^0.4.4",
"tslib": "^1.10.0" "tslib": "^1.10.0"
} }
}, },
"@apollo/react-hoc": { "@apollo/react-hoc": {
"version": "3.1.3", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/@apollo/react-hoc/-/react-hoc-3.1.3.tgz", "resolved": "https://registry.npmjs.org/@apollo/react-hoc/-/react-hoc-3.1.5.tgz",
"integrity": "sha512-oCPma0uBVPTcYTR5sOvtMbpaWll4xDBvYfKr6YkDorUcQVeNzFu1LK1kmQjJP64bKsaziKYji5ibFaeCnVptmA==", "integrity": "sha512-jlZ2pvEnRevLa54H563BU0/xrYSgWQ72GksarxUzCHQW85nmn9wQln0kLBX7Ua7SBt9WgiuYQXQVechaaCulfQ==",
"requires": { "requires": {
"@apollo/react-common": "^3.1.3", "@apollo/react-common": "^3.1.4",
"@apollo/react-components": "^3.1.3", "@apollo/react-components": "^3.1.5",
"hoist-non-react-statics": "^3.3.0", "hoist-non-react-statics": "^3.3.0",
"ts-invariant": "^0.4.4", "ts-invariant": "^0.4.4",
"tslib": "^1.10.0" "tslib": "^1.10.0"
} }
}, },
"@apollo/react-hooks": { "@apollo/react-hooks": {
"version": "3.1.3", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/@apollo/react-hooks/-/react-hooks-3.1.3.tgz", "resolved": "https://registry.npmjs.org/@apollo/react-hooks/-/react-hooks-3.1.5.tgz",
"integrity": "sha512-reIRO9xKdfi+B4gT/o/hnXuopUnm7WED/ru8VQydPw+C/KG/05Ssg1ZdxFKHa3oxwiTUIDnevtccIH35POanbA==", "integrity": "sha512-y0CJ393DLxIIkksRup4nt+vSjxalbZBXnnXxYbviq/woj+zKa431zy0yT4LqyRKpFy9ahMIwxBnBwfwIoupqLQ==",
"requires": { "requires": {
"@apollo/react-common": "^3.1.3", "@apollo/react-common": "^3.1.4",
"@wry/equality": "^0.1.9", "@wry/equality": "^0.1.9",
"ts-invariant": "^0.4.4", "ts-invariant": "^0.4.4",
"tslib": "^1.10.0" "tslib": "^1.10.0"
} }
}, },
"@apollo/react-ssr": { "@apollo/react-ssr": {
"version": "3.1.3", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/@apollo/react-ssr/-/react-ssr-3.1.3.tgz", "resolved": "https://registry.npmjs.org/@apollo/react-ssr/-/react-ssr-3.1.5.tgz",
"integrity": "sha512-fUTmEYHxSTX1GA43B8vICxXXplpcEBnDwn0IgdAc3eG0p2YK97ZrJDRFCJ5vD7fyDZsrYhMf+rAI3sd+H2SS+A==", "integrity": "sha512-wuLPkKlctNn3u8EU8rlECyktpOUCeekFfb0KhIKknpGY6Lza2Qu0bThx7D9MIbVEzhKadNNrzLcpk0Y8/5UuWg==",
"requires": { "requires": {
"@apollo/react-common": "^3.1.3", "@apollo/react-common": "^3.1.4",
"@apollo/react-hooks": "^3.1.3", "@apollo/react-hooks": "^3.1.5",
"tslib": "^1.10.0" "tslib": "^1.10.0"
} }
}, },
@ -17593,15 +17593,15 @@
} }
}, },
"react-apollo": { "react-apollo": {
"version": "3.1.3", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/react-apollo/-/react-apollo-3.1.3.tgz", "resolved": "https://registry.npmjs.org/react-apollo/-/react-apollo-3.1.5.tgz",
"integrity": "sha512-orCZNoAkgveaK5b75y7fw1MSqSHOU/Wuu9rRFOGmRQBSQVZjvV4DI+hj604rHmuN9+WDABxb5W48wTa0F/xNZQ==", "integrity": "sha512-xOxMqxORps+WHrUYbjVHPliviomefOpu5Sh35oO3osuOyPTxvrljdfTLGCggMhcXBsDljtS5Oy4g+ijWg3D4JQ==",
"requires": { "requires": {
"@apollo/react-common": "^3.1.3", "@apollo/react-common": "^3.1.4",
"@apollo/react-components": "^3.1.3", "@apollo/react-components": "^3.1.5",
"@apollo/react-hoc": "^3.1.3", "@apollo/react-hoc": "^3.1.5",
"@apollo/react-hooks": "^3.1.3", "@apollo/react-hooks": "^3.1.5",
"@apollo/react-ssr": "^3.1.3" "@apollo/react-ssr": "^3.1.5"
} }
}, },
"react-clientside-effect": { "react-clientside-effect": {

View file

@ -48,7 +48,7 @@
"moment-timezone": "^0.5.26", "moment-timezone": "^0.5.26",
"qs": "^6.9.0", "qs": "^6.9.0",
"react": "^16.12.0", "react": "^16.12.0",
"react-apollo": "^3.1.3", "react-apollo": "^3.1.4",
"react-dom": "^16.9.0", "react-dom": "^16.9.0",
"react-dropzone": "^8.2.0", "react-dropzone": "^8.2.0",
"react-error-boundary": "^1.2.5", "react-error-boundary": "^1.2.5",

View file

@ -19,8 +19,8 @@ const selectedAttributes = [1, 4, 5].map(index => attributes[index]);
const selectedWarehouses = [0, 1, 3].map(index => warehouseList[index]); const selectedWarehouses = [0, 1, 3].map(index => warehouseList[index]);
const price: AllOrAttribute<string> = { const price: AllOrAttribute<string> = {
all: false,
attribute: selectedAttributes[0].id, attribute: selectedAttributes[0].id,
mode: "attribute",
value: "2.79", value: "2.79",
values: selectedAttributes[0].values.map((attribute, attributeIndex) => ({ values: selectedAttributes[0].values.map((attribute, attributeIndex) => ({
slug: attribute.slug, slug: attribute.slug,
@ -29,8 +29,8 @@ const price: AllOrAttribute<string> = {
}; };
const stock: AllOrAttribute<number[]> = { const stock: AllOrAttribute<number[]> = {
all: false,
attribute: selectedAttributes[0].id, attribute: selectedAttributes[0].id,
mode: "attribute",
value: selectedWarehouses.map( value: selectedWarehouses.map(
(_, warehouseIndex) => (warehouseIndex + 2) * 3 (_, warehouseIndex) => (warehouseIndex + 2) * 3
), ),
@ -88,8 +88,28 @@ const props: ProductVariantCreatorContentProps = {
storiesOf("Views / Products / Create multiple variants", module) storiesOf("Views / Products / Create multiple variants", module)
.addDecorator(storyFn => <Container>{storyFn()}</Container>) .addDecorator(storyFn => <Container>{storyFn()}</Container>)
.addDecorator(Decorator) .addDecorator(Decorator)
.add("choose values", () => <ProductVariantCreatorContent {...props} />) .add("choose values", () => <ProductVariantCreatorContent {...props} />);
.add("prices and SKU", () => (
storiesOf(
"Views / Products / Create multiple variants / prices and SKUs",
module
)
.addDecorator(storyFn => <Container>{storyFn()}</Container>)
.addDecorator(Decorator)
.add("apply to all", () => (
<ProductVariantCreatorContent
{...props}
data={{
...data,
stock: {
...data.stock,
mode: "all"
}
}}
step={ProductVariantCreatorStep.prices}
/>
))
.add("apply to attribute", () => (
<ProductVariantCreatorContent <ProductVariantCreatorContent
{...props} {...props}
step={ProductVariantCreatorStep.prices} step={ProductVariantCreatorStep.prices}

View file

@ -2,8 +2,8 @@ import React from "react";
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
import { ProductVariantBulkCreate_productVariantBulkCreate_errors } from "@saleor/products/types/ProductVariantBulkCreate"; import { ProductVariantBulkCreate_productVariantBulkCreate_errors } from "@saleor/products/types/ProductVariantBulkCreate";
import { isSelected } from "@saleor/utils/lists";
import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment"; import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment";
import { isSelected } from "@saleor/utils/lists";
import { ProductVariantCreateFormData } from "./form"; import { ProductVariantCreateFormData } from "./form";
import ProductVariantCreatePriceAndSku from "./ProductVariantCreatorPriceAndSku"; import ProductVariantCreatePriceAndSku from "./ProductVariantCreatorPriceAndSku";
import ProductVariantCreateSummary from "./ProductVariantCreatorSummary"; import ProductVariantCreateSummary from "./ProductVariantCreatorSummary";
@ -64,10 +64,11 @@ const ProductVariantCreatorContent: React.FC<ProductVariantCreatorContentProps>
attributes={selectedAttributes} attributes={selectedAttributes}
currencySymbol={currencySymbol} currencySymbol={currencySymbol}
data={data} data={data}
onApplyToAllChange={(all, type) => warehouses={warehouses}
onApplyToAllChange={(mode, type) =>
dispatchFormDataAction({ dispatchFormDataAction({
applyPriceOrStockToAll: { applyPriceOrStockToAll: {
all mode
}, },
type: type:
type === "price" type === "price"
@ -109,6 +110,14 @@ const ProductVariantCreatorContent: React.FC<ProductVariantCreatorContentProps>
} }
) )
} }
onWarehouseToggle={warehouseId =>
dispatchFormDataAction({
changeWarehouses: {
warehouseId
},
type: ProductVariantCreateReducerActionType.changeWarehouses
})
}
/> />
)} )}
{step === ProductVariantCreatorStep.summary && ( {step === ProductVariantCreatorStep.summary && (

View file

@ -39,20 +39,22 @@ function canHitNext(
case ProductVariantCreatorStep.values: case ProductVariantCreatorStep.values:
return data.attributes.every(attribute => attribute.values.length > 0); return data.attributes.every(attribute => attribute.values.length > 0);
case ProductVariantCreatorStep.prices: case ProductVariantCreatorStep.prices:
if (data.price.all) { if (data.price.mode === "all") {
if (data.price.value === "") { if (data.price.value === "") {
return false; return false;
} }
} else { } else if (data.price.mode === "attribute") {
if ( if (
data.price.attribute === "" || data.price.attribute === "" ||
data.price.values.some(attributeValue => attributeValue.value === "") data.price.values.some(attributeValue => attributeValue.value === "")
) { ) {
return false; return false;
} }
} else {
return true;
} }
if (!data.stock.all || data.stock.attribute) { if (data.stock.mode === "attribute" || data.stock.attribute) {
return false; return false;
} }

View file

@ -2,7 +2,11 @@ import React from "react";
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
import { ProductVariantCreateFormData } from "./form"; import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment";
import {
ProductVariantCreateFormData,
VariantCreatorPricesAndSkuMode
} from "./form";
import ProductVariantCreatorPrices from "./ProductVariantCreatorPrices"; import ProductVariantCreatorPrices from "./ProductVariantCreatorPrices";
import ProductVariantCreatorStock from "./ProductVariantCreatorStock"; import ProductVariantCreatorStock from "./ProductVariantCreatorStock";
@ -11,7 +15,11 @@ export interface ProductVariantCreatorPriceAndSkuProps {
attributes: ProductDetails_product_productType_variantAttributes[]; attributes: ProductDetails_product_productType_variantAttributes[];
currencySymbol: string; currencySymbol: string;
data: ProductVariantCreateFormData; data: ProductVariantCreateFormData;
onApplyToAllChange: (applyToAll: boolean, type: PriceOrStock) => void; warehouses: WarehouseFragment[];
onApplyToAllChange: (
value: VariantCreatorPricesAndSkuMode,
type: PriceOrStock
) => void;
onApplyToAllPriceOrStockChange: (value: string, type: PriceOrStock) => void; onApplyToAllPriceOrStockChange: (value: string, type: PriceOrStock) => void;
onAttributeSelect: (id: string, type: PriceOrStock) => void; onAttributeSelect: (id: string, type: PriceOrStock) => void;
onAttributeValueChange: ( onAttributeValueChange: (
@ -19,16 +27,19 @@ export interface ProductVariantCreatorPriceAndSkuProps {
value: string, value: string,
type: PriceOrStock type: PriceOrStock
) => void; ) => void;
onWarehouseToggle: (id: string) => void;
} }
const ProductVariantCreatorPriceAndSku: React.FC<ProductVariantCreatorPriceAndSkuProps> = ({ const ProductVariantCreatorPriceAndSku: React.FC<ProductVariantCreatorPriceAndSkuProps> = ({
attributes, attributes,
currencySymbol, currencySymbol,
data, data,
warehouses,
onApplyToAllPriceOrStockChange, onApplyToAllPriceOrStockChange,
onApplyToAllChange, onApplyToAllChange,
onAttributeSelect, onAttributeSelect,
onAttributeValueChange onAttributeValueChange,
onWarehouseToggle
}) => ( }) => (
<> <>
<ProductVariantCreatorPrices <ProductVariantCreatorPrices
@ -48,6 +59,7 @@ const ProductVariantCreatorPriceAndSku: React.FC<ProductVariantCreatorPriceAndSk
<ProductVariantCreatorStock <ProductVariantCreatorStock
attributes={attributes} attributes={attributes}
data={data} data={data}
warehouses={warehouses}
onApplyToAllChange={value => onApplyToAllChange(value, "stock")} onApplyToAllChange={value => onApplyToAllChange(value, "stock")}
onApplyToAllStockChange={value => onApplyToAllStockChange={value =>
onApplyToAllPriceOrStockChange(value, "stock") onApplyToAllPriceOrStockChange(value, "stock")
@ -56,6 +68,7 @@ const ProductVariantCreatorPriceAndSku: React.FC<ProductVariantCreatorPriceAndSk
onAttributeValueChange={(id, value) => onAttributeValueChange={(id, value) =>
onAttributeValueChange(id, value, "stock") onAttributeValueChange(id, value, "stock")
} }
onWarehouseToggle={onWarehouseToggle}
/> />
</> </>
); );

View file

@ -15,7 +15,10 @@ import Hr from "@saleor/components/Hr";
import SingleSelectField from "@saleor/components/SingleSelectField"; import SingleSelectField from "@saleor/components/SingleSelectField";
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import { ProductVariantCreateFormData } from "./form"; import {
ProductVariantCreateFormData,
VariantCreatorPricesAndSkuMode
} from "./form";
import { getPriceAttributeValues } from "./utils"; import { getPriceAttributeValues } from "./utils";
const useStyles = makeStyles( const useStyles = makeStyles(
@ -41,7 +44,7 @@ export interface ProductVariantCreatorPricesProps {
attributes: ProductDetails_product_productType_variantAttributes[]; attributes: ProductDetails_product_productType_variantAttributes[];
currencySymbol: string; currencySymbol: string;
data: ProductVariantCreateFormData; data: ProductVariantCreateFormData;
onApplyToAllChange: (applyToAll: boolean) => void; onApplyToAllChange: (applyToAll: VariantCreatorPricesAndSkuMode) => void;
onApplyToAllPriceChange: (value: string) => void; onApplyToAllPriceChange: (value: string) => void;
onAttributeSelect: (id: string) => void; onAttributeSelect: (id: string) => void;
onAttributeValueChange: (id: string, value: string) => void; onAttributeValueChange: (id: string, value: string) => void;
@ -75,14 +78,14 @@ const ProductVariantCreatorPrices: React.FC<ProductVariantCreatorPricesProps> =
})} })}
/> />
<CardContent> <CardContent>
<RadioGroup value={data.price.all ? "applyToAll" : "applyToAttribute"}> <RadioGroup value={data.price.mode}>
<FormControlLabel <FormControlLabel
value="applyToAll" value="all"
control={<Radio color="primary" />} control={<Radio color="primary" />}
label={intl.formatMessage({ label={intl.formatMessage({
defaultMessage: "Apply single price to all SKUs" defaultMessage: "Apply single price to all SKUs"
})} })}
onChange={() => onApplyToAllChange(true)} onChange={() => onApplyToAllChange("all")}
/> />
<FormSpacer /> <FormSpacer />
<TextField <TextField
@ -103,15 +106,15 @@ const ProductVariantCreatorPrices: React.FC<ProductVariantCreatorPricesProps> =
/> />
<FormSpacer /> <FormSpacer />
<FormControlLabel <FormControlLabel
value="applyToAttribute" value="attribute"
control={<Radio color="primary" />} control={<Radio color="primary" />}
label={intl.formatMessage({ label={intl.formatMessage({
defaultMessage: "Apply unique prices by attribute to each SKU" defaultMessage: "Apply unique prices by attribute to each SKU"
})} })}
onChange={() => onApplyToAllChange(false)} onChange={() => onApplyToAllChange("attribute")}
/> />
</RadioGroup> </RadioGroup>
{!data.price.all && ( {data.price.mode === "attribute" && (
<> <>
<FormSpacer /> <FormSpacer />
<Grid variant="uniform"> <Grid variant="uniform">

View file

@ -15,7 +15,14 @@ import Hr from "@saleor/components/Hr";
import SingleSelectField from "@saleor/components/SingleSelectField"; import SingleSelectField from "@saleor/components/SingleSelectField";
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import { ProductVariantCreateFormData } from "./form"; import { WarehouseFragment } from "@saleor/warehouses/types/WarehouseFragment";
import CardSpacer from "@saleor/components/CardSpacer";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import { isSelected, toggle } from "@saleor/utils/lists";
import {
ProductVariantCreateFormData,
VariantCreatorPricesAndSkuMode
} from "./form";
import { getStockAttributeValues } from "./utils"; import { getStockAttributeValues } from "./utils";
const useStyles = makeStyles( const useStyles = makeStyles(
@ -32,6 +39,30 @@ const useStyles = makeStyles(
}, },
shortInput: { shortInput: {
width: "33%" width: "33%"
},
stockContainer: {
columnGap: theme.spacing(3) + "px",
display: "grid",
gridTemplateColumns: "repeat(3, 288px)",
rowGap: theme.spacing(2) + "px"
},
stockHeader: {
marginBottom: theme.spacing()
},
warehouseContainer: {
columnGap: theme.spacing(3) + "px",
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
rowGap: theme.spacing(2) + "px"
},
warehouseHeader: {
marginBottom: theme.spacing()
},
warehouseName: {
marginBottom: theme.spacing()
},
warehouseSubheader: {
marginBottom: theme.spacing(2)
} }
}), }),
{ name: "ProductVariantCreatorStock" } { name: "ProductVariantCreatorStock" }
@ -40,20 +71,24 @@ const useStyles = makeStyles(
export interface ProductVariantCreatorStockProps { export interface ProductVariantCreatorStockProps {
attributes: ProductDetails_product_productType_variantAttributes[]; attributes: ProductDetails_product_productType_variantAttributes[];
data: ProductVariantCreateFormData; data: ProductVariantCreateFormData;
onApplyToAllChange: (applyToAll: boolean) => void; warehouses: WarehouseFragment[];
onApplyToAllChange: (mode: VariantCreatorPricesAndSkuMode) => void;
onApplyToAllStockChange: (value: string) => void; onApplyToAllStockChange: (value: string) => void;
onAttributeSelect: (id: string) => void; onAttributeSelect: (id: string) => void;
onAttributeValueChange: (id: string, value: string) => void; onAttributeValueChange: (id: string, value: string) => void;
onWarehouseToggle: (id: string) => void;
} }
const ProductVariantCreatorStock: React.FC<ProductVariantCreatorStockProps> = props => { const ProductVariantCreatorStock: React.FC<ProductVariantCreatorStockProps> = props => {
const { const {
attributes, attributes,
data, data,
warehouses,
onApplyToAllChange, onApplyToAllChange,
onApplyToAllStockChange, onApplyToAllStockChange,
onAttributeSelect, onAttributeSelect,
onAttributeValueChange onAttributeValueChange,
onWarehouseToggle
} = props; } = props;
const classes = useStyles(props); const classes = useStyles(props);
const intl = useIntl(); const intl = useIntl();
@ -73,18 +108,78 @@ const ProductVariantCreatorStock: React.FC<ProductVariantCreatorStockProps> = pr
})} })}
/> />
<CardContent> <CardContent>
<RadioGroup value={data.stock.all ? "applyToAll" : "applyToAttribute"}> {warehouses.length > 1 && (
<>
<Typography className={classes.warehouseHeader} variant="h5">
<FormattedMessage
defaultMessage="Warehouses"
description="header"
id="productVariantCreatorStockSectionHeader"
/>
</Typography>
<Typography className={classes.warehouseSubheader}>
<FormattedMessage
defaultMessage="Based on your selections we will create {numberOfProducts} products. Use this step to customize price and stocks for your new products"
id="productVariantCreatorStockSectionHeader"
values={{
numberOfProducts: data.attributes.reduce(
(acc, attr) => acc + attr.values.length,
0
)
}}
/>
</Typography>
<div className={classes.warehouseContainer}>
{warehouses.map(warehouse => (
<ControlledCheckbox
checked={isSelected(
warehouse.id,
data.warehouses,
(a, b) => a === b
)}
name={`warehouse:${warehouse.id}`}
label={warehouse.name}
onChange={() => onWarehouseToggle(warehouse.id)}
key={warehouse.id}
/>
))}
</div>
<CardSpacer />
<Hr />
<CardSpacer />
</>
)}
<Typography className={classes.stockHeader} variant="h5">
<FormattedMessage
defaultMessage="Stock"
description="variant stock, header"
id="productVariantCreatorStockSectionHeader"
/>
</Typography>
<RadioGroup value={data.stock.mode}>
<FormControlLabel <FormControlLabel
value="applyToAll" value="all"
control={<Radio color="primary" />} control={<Radio color="primary" />}
label={intl.formatMessage({ label={intl.formatMessage({
defaultMessage: "Apply single stock to all SKUs" defaultMessage: "Apply single stock to all SKUs"
})} })}
onChange={() => onApplyToAllChange(true)} onChange={() => onApplyToAllChange("all")}
/> />
<FormSpacer /> {data.stock.mode === "all" && (
<div className={classes.stockContainer}>
{data.warehouses.map((warehouseId, warehouseIndex) => (
<div>
<Typography
className={classes.warehouseName}
key={warehouseId}
>
{
warehouses.find(warehouse => warehouse.id === warehouseId)
.name
}
</Typography>
<TextField <TextField
className={classes.shortInput} fullWidth
inputProps={{ inputProps={{
min: 0, min: 0,
type: "number" type: "number"
@ -93,20 +188,25 @@ const ProductVariantCreatorStock: React.FC<ProductVariantCreatorStockProps> = pr
defaultMessage: "Stock", defaultMessage: "Stock",
id: "productVariantCreatePricesStockInputLabel" id: "productVariantCreatePricesStockInputLabel"
})} })}
value={data.stock.value} value={data.stock.value[warehouseIndex]}
onChange={event => onApplyToAllStockChange(event.target.value)} onChange={event =>
onApplyToAllStockChange(event.target.value)
}
/> />
</div>
))}
</div>
)}
<FormSpacer /> <FormSpacer />
<FormControlLabel <FormControlLabel
value="applyToAttribute" value="attribute"
control={<Radio color="primary" />} control={<Radio color="primary" />}
label={intl.formatMessage({ label={intl.formatMessage({
defaultMessage: "Apply unique stock by attribute to each SKU" defaultMessage: "Apply unique stock by attribute to each SKU"
})} })}
onChange={() => onApplyToAllChange(false)} onChange={() => onApplyToAllChange("attribute")}
/> />
</RadioGroup> {data.stock.mode === "attribute" && (
{!data.stock.all && (
<> <>
<FormSpacer /> <FormSpacer />
<Grid variant="uniform"> <Grid variant="uniform">
@ -165,6 +265,16 @@ const ProductVariantCreatorStock: React.FC<ProductVariantCreatorStockProps> = pr
))} ))}
</> </>
)} )}
<FormSpacer />
<FormControlLabel
value="skip"
control={<Radio color="primary" />}
label={intl.formatMessage({
defaultMessage: "Skip stock for now"
})}
onChange={() => onApplyToAllChange("skip")}
/>
</RadioGroup>
</CardContent> </CardContent>
</Card> </Card>
); );

View file

@ -52,7 +52,7 @@ const ProductVariantCreatorValues: React.FC<ProductVariantCreatorValuesProps> =
value.slug, 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.slug}`} name={`value:${value.slug}`}

View file

@ -26,16 +26,32 @@ function getAttributeValuePriceOrStock<T>(
return attributeValue.value; return attributeValue.value;
} }
function getValueFromMode<T>(
attributes: CreateVariantInput,
priceOrStock: AllOrAttribute<T>,
skipValue: T
): T {
switch (priceOrStock.mode) {
case "all":
return priceOrStock.value;
case "attribute":
return getAttributeValuePriceOrStock(attributes, priceOrStock);
case "skip":
return skipValue;
}
}
function createVariant( function createVariant(
data: ProductVariantCreateFormData, data: ProductVariantCreateFormData,
attributes: CreateVariantInput attributes: CreateVariantInput
): ProductVariantBulkCreateInput { ): ProductVariantBulkCreateInput {
const priceOverride = data.price.all const priceOverride = getValueFromMode(attributes, data.price, "0");
? data.price.value const stocks = getValueFromMode(
: getAttributeValuePriceOrStock(attributes, data.price); attributes,
const stocks = data.stock.all data.stock,
? data.stock.value data.warehouses.map(() => 0)
: getAttributeValuePriceOrStock(attributes, data.stock); );
return { return {
attributes: attributes.map(attribute => ({ attributes: attributes.map(attribute => ({
id: attribute.attributeId, id: attribute.attributeId,
@ -91,8 +107,8 @@ export function createVariants(
data: ProductVariantCreateFormData data: ProductVariantCreateFormData
): ProductVariantBulkCreateInput[] { ): ProductVariantBulkCreateInput[] {
if ( if (
(!data.price.all && !data.price.attribute) || (data.price.mode === "attribute" && !data.price.attribute) ||
(!data.stock.all && !data.stock.attribute) (data.stock.mode === "attribute" && !data.stock.attribute)
) { ) {
return []; return [];
} }

View file

@ -6,8 +6,9 @@ export interface AttributeValue<T> {
slug: string; slug: string;
value: T; value: T;
} }
export type VariantCreatorPricesAndSkuMode = "all" | "attribute" | "skip";
export interface AllOrAttribute<T> { export interface AllOrAttribute<T> {
all: boolean; mode: VariantCreatorPricesAndSkuMode;
attribute: string; attribute: string;
value: T; value: T;
values: Array<AttributeValue<T>>; values: Array<AttributeValue<T>>;
@ -34,14 +35,14 @@ export const createInitialForm = (
values: [] values: []
})), })),
price: { price: {
all: true,
attribute: undefined, attribute: undefined,
mode: "all",
value: price || "", value: price || "",
values: [] values: []
}, },
stock: { stock: {
all: true,
attribute: undefined, attribute: undefined,
mode: "all",
value: warehouses.length === 1 ? [0] : [], value: warehouses.length === 1 ? [0] : [],
values: [] values: []
}, },

View file

@ -8,11 +8,16 @@ import {
} from "@saleor/utils/lists"; } from "@saleor/utils/lists";
import { StockInput } from "@saleor/types/globalTypes"; import { StockInput } from "@saleor/types/globalTypes";
import { createVariants } from "./createVariants"; import { createVariants } from "./createVariants";
import { ProductVariantCreateFormData } from "./form"; import {
ProductVariantCreateFormData,
VariantCreatorPricesAndSkuMode
} from "./form";
export enum ProductVariantCreateReducerActionType { export enum ProductVariantCreateReducerActionType {
applyPriceToAll, applyPriceToAll,
applyPriceToAttribute,
applyStockToAll, applyStockToAll,
applyStockToAttribute,
changeApplyPriceToAllValue, changeApplyPriceToAllValue,
changeApplyPriceToAttributeId, changeApplyPriceToAttributeId,
changeApplyStockToAllValue, changeApplyStockToAllValue,
@ -21,6 +26,7 @@ export enum ProductVariantCreateReducerActionType {
changeAttributeValueStock, changeAttributeValueStock,
changeVariantData, changeVariantData,
changeVariantStockData, changeVariantStockData,
changeWarehouses,
deleteVariant, deleteVariant,
reload, reload,
selectValue selectValue
@ -28,7 +34,7 @@ export enum ProductVariantCreateReducerActionType {
export type VariantField = "price" | "sku"; export type VariantField = "price" | "sku";
export interface ProductVariantCreateReducerAction { export interface ProductVariantCreateReducerAction {
applyPriceOrStockToAll?: { applyPriceOrStockToAll?: {
all: boolean; mode: VariantCreatorPricesAndSkuMode;
}; };
changeApplyPriceToAllValue?: { changeApplyPriceToAllValue?: {
price: string; price: string;
@ -52,6 +58,9 @@ export interface ProductVariantCreateReducerAction {
stock: StockInput; stock: StockInput;
variantIndex: number; variantIndex: number;
}; };
changeWarehouses?: {
warehouseId: string;
};
deleteVariant?: { deleteVariant?: {
variantIndex: number; variantIndex: number;
}; };
@ -119,26 +128,26 @@ function selectValue(
function applyPriceToAll( function applyPriceToAll(
state: ProductVariantCreateFormData, state: ProductVariantCreateFormData,
value: boolean mode: VariantCreatorPricesAndSkuMode
): ProductVariantCreateFormData { ): ProductVariantCreateFormData {
return { return {
...state, ...state,
price: { price: {
...state.price, ...state.price,
all: value mode
} }
}; };
} }
function applyStockToAll( function applyStockToAll(
state: ProductVariantCreateFormData, state: ProductVariantCreateFormData,
value: boolean mode: VariantCreatorPricesAndSkuMode
): ProductVariantCreateFormData { ): ProductVariantCreateFormData {
return { return {
...state, ...state,
stock: { stock: {
...state.stock, ...state.stock,
all: value mode
} }
}; };
} }
@ -318,6 +327,16 @@ function changeVariantStockData(
}; };
} }
function changeWarehouses(
state: ProductVariantCreateFormData,
warehouseId: string
): ProductVariantCreateFormData {
return {
...state,
warehouses: toggle(warehouseId, state.warehouses, (a, b) => a === b)
};
}
function deleteVariant( function deleteVariant(
state: ProductVariantCreateFormData, state: ProductVariantCreateFormData,
variantIndex: number variantIndex: number
@ -349,9 +368,9 @@ function reduceProductVariantCreateFormData(
action.selectValue.valueId action.selectValue.valueId
); );
case ProductVariantCreateReducerActionType.applyPriceToAll: case ProductVariantCreateReducerActionType.applyPriceToAll:
return applyPriceToAll(prevState, action.applyPriceOrStockToAll.all); return applyPriceToAll(prevState, action.applyPriceOrStockToAll.mode);
case ProductVariantCreateReducerActionType.applyStockToAll: case ProductVariantCreateReducerActionType.applyStockToAll:
return applyStockToAll(prevState, action.applyPriceOrStockToAll.all); return applyStockToAll(prevState, action.applyPriceOrStockToAll.mode);
case ProductVariantCreateReducerActionType.changeAttributeValuePrice: case ProductVariantCreateReducerActionType.changeAttributeValuePrice:
return changeAttributeValuePrice( return changeAttributeValuePrice(
prevState, prevState,
@ -383,8 +402,8 @@ function reduceProductVariantCreateFormData(
case ProductVariantCreateReducerActionType.changeApplyStockToAllValue: case ProductVariantCreateReducerActionType.changeApplyStockToAllValue:
return changeApplyStockToAllValue( return changeApplyStockToAllValue(
prevState, prevState,
action.changeApplyStockToAllValue.quantity, action.changeApplyStockToAllValue.warehouseIndex,
action.changeApplyStockToAllValue.warehouseIndex action.changeApplyStockToAllValue.quantity
); );
case ProductVariantCreateReducerActionType.changeVariantData: case ProductVariantCreateReducerActionType.changeVariantData:
return changeVariantData( return changeVariantData(
@ -399,6 +418,8 @@ function reduceProductVariantCreateFormData(
action.changeVariantStockData.stock, action.changeVariantStockData.stock,
action.changeVariantStockData.variantIndex action.changeVariantStockData.variantIndex
); );
case ProductVariantCreateReducerActionType.changeWarehouses:
return changeWarehouses(prevState, action.changeWarehouses.warehouseId);
case ProductVariantCreateReducerActionType.deleteVariant: case ProductVariantCreateReducerActionType.deleteVariant:
return deleteVariant(prevState, action.deleteVariant.variantIndex); return deleteVariant(prevState, action.deleteVariant.variantIndex);
case ProductVariantCreateReducerActionType.reload: case ProductVariantCreateReducerActionType.reload:

View file

@ -103,6 +103,7 @@ const productVariantAttributesFragment = gql`
} }
} }
productType { productType {
id
variantAttributes { variantAttributes {
id id
name name

View file

@ -45,6 +45,9 @@ export function updateAtIndex<TData>(
list: List<TData>, list: List<TData>,
index: number index: number
) { ) {
if (!index.toFixed) {
throw new Error("Index is not a number");
}
return addAtIndex(data, removeAtIndex(list, index), index); return addAtIndex(data, removeAtIndex(list, index), index);
} }