Add variant matrix generation

This commit is contained in:
dominik-zeglen 2019-09-23 17:19:41 +02:00
parent 742523bac7
commit 2879a80d0d
14 changed files with 1076 additions and 148 deletions

View file

@ -4,55 +4,56 @@ 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 { isSelected } from "@saleor/utils/lists";
import Decorator from "../../../storybook/Decorator"; import Decorator from "../../../storybook/Decorator";
import { createVariants } from "./createVariants";
import { AllOrAttribute } from "./form";
import ProductVariantCreateContent, { import ProductVariantCreateContent, {
ProductVariantCreateContentProps ProductVariantCreateContentProps
} from "./ProductVariantCreateContent"; } from "./ProductVariantCreateContent";
import ProductVariantCreateDialog from "./ProductVariantCreateDialog"; import ProductVariantCreateDialog from "./ProductVariantCreateDialog";
const selectedAttributes = [1, 2, 4].map(index => attributes[index].id); const selectedAttributes = [1, 4, 5].map(index => attributes[index]);
const selectedValues = attributes
.filter(attribute => const price: AllOrAttribute = {
isSelected(attribute.id, selectedAttributes, (a, b) => a === b) all: false,
) attribute: selectedAttributes[1].id,
.map(attribute => attribute.values.map(value => value.id)) value: "2.79",
.reduce((acc, curr) => [...acc, ...curr], []) values: selectedAttributes[1].values.map((attribute, attributeIndex) => ({
.filter((_, valueIndex) => valueIndex % 2); id: attribute.id,
value: (attributeIndex + 4).toFixed(2)
}))
};
const stock: AllOrAttribute = {
all: false,
attribute: selectedAttributes[1].id,
value: "8",
values: selectedAttributes[1].values.map((attribute, attributeIndex) => ({
id: attribute.id,
value: (selectedAttributes.length * 10 - attributeIndex).toString()
}))
};
const dataAttributes = selectedAttributes.map(attribute => ({
id: attribute.id,
values: attribute.values
.map(value => value.id)
.filter((_, valueIndex) => valueIndex % 2 !== 1)
}));
const props: ProductVariantCreateContentProps = { const props: ProductVariantCreateContentProps = {
attributes, attributes,
currencySymbol: "USD", currencySymbol: "USD",
data: { data: {
attributes: selectedAttributes, attributes: dataAttributes,
price: { price,
all: false, stock,
attribute: selectedAttributes[1], variants: createVariants({
value: "2.79", attributes: dataAttributes,
values: selectedAttributes.map((_, attributeIndex) => price,
(attributeIndex + 4).toFixed(2) stock,
) variants: []
}, })
stock: {
all: false,
attribute: selectedAttributes[1],
value: "8",
values: selectedAttributes.map((_, attributeIndex) =>
(selectedAttributes.length * 10 - attributeIndex).toString()
)
},
values: selectedValues,
variants: [
{
attributes: attributes
.filter(attribute => selectedAttributes.includes(attribute.id))
.map(attribute => ({
id: attribute.id,
values: [attribute.values[0].id]
})),
product: "=1uahc98nas"
}
]
}, },
dispatchFormDataAction: () => undefined, dispatchFormDataAction: () => undefined,
step: "attributes" step: "attributes"
@ -64,7 +65,7 @@ storiesOf("Views / Products / Create multiple variants", module)
style={{ style={{
margin: "auto", margin: "auto",
overflow: "visible", overflow: "visible",
width: 600 width: 800
}} }}
> >
<CardContent>{storyFn()}</CardContent> <CardContent>{storyFn()}</CardContent>

View file

@ -43,7 +43,7 @@ const ProductVariantCreateAttributes: React.FC<
return null; return null;
} }
const isChecked = !!data.attributes.find( const isChecked = !!data.attributes.find(
selectedAttribute => selectedAttribute === attribute.id selectedAttribute => selectedAttribute.id === attribute.id
); );
return ( return (

View file

@ -40,7 +40,11 @@ const ProductVariantCreateContent: React.FC<
const classes = useStyles(props); const classes = useStyles(props);
const selectedAttributes = attributes.filter(attribute => const selectedAttributes = attributes.filter(attribute =>
isSelected(attribute.id, data.attributes, (a, b) => a === b) isSelected(
attribute.id,
data.attributes.map(dataAttribute => dataAttribute.id),
(a, b) => a === b
)
); );
return ( return (
@ -51,9 +55,9 @@ const ProductVariantCreateContent: React.FC<
<ProductVariantCreateAttributes <ProductVariantCreateAttributes
attributes={attributes} attributes={attributes}
data={data} data={data}
onAttributeClick={id => onAttributeClick={attributeId =>
dispatchFormDataAction({ dispatchFormDataAction({
id, attributeId,
type: "selectAttribute" type: "selectAttribute"
}) })
} }
@ -63,10 +67,11 @@ const ProductVariantCreateContent: React.FC<
<ProductVariantCreateValues <ProductVariantCreateValues
attributes={selectedAttributes} attributes={selectedAttributes}
data={data} data={data}
onValueClick={id => onValueClick={(attributeId, valueId) =>
dispatchFormDataAction({ dispatchFormDataAction({
id, attributeId,
type: "selectValue" type: "selectValue",
valueId
}) })
} }
/> />
@ -90,19 +95,23 @@ const ProductVariantCreateContent: React.FC<
value value
}) })
} }
onAttributeSelect={(id, type) => onAttributeSelect={(attributeId, type) =>
dispatchFormDataAction({ dispatchFormDataAction({
id, attributeId,
type: type:
type === "price" type === "price"
? "changeApplyPriceToAttributeId" ? "changeApplyPriceToAttributeId"
: "changeApplyStockToAttributeId" : "changeApplyStockToAttributeId"
}) })
} }
onValueClick={id => onAttributeValueChange={(valueId, value, type) =>
dispatchFormDataAction({ dispatchFormDataAction({
id, type:
type: "selectValue" type === "price"
? "changeAttributeValuePrice"
: "changeAttributeValueStock",
value,
valueId
}) })
} }
/> />

View file

@ -21,7 +21,7 @@ const useStyles = makeStyles((theme: Theme) => ({
content: { content: {
overflowX: "visible", overflowX: "visible",
overflowY: "hidden", overflowY: "hidden",
width: 600 width: 800
} }
})); }));

View file

@ -13,7 +13,6 @@ import Grid from "@saleor/components/Grid";
import Hr from "@saleor/components/Hr"; 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 { isSelected } from "@saleor/utils/lists";
import { ProductVariantCreateFormData } from "./form"; import { ProductVariantCreateFormData } from "./form";
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles((theme: Theme) => ({
@ -33,10 +32,14 @@ export type PriceOrStock = "price" | "stock";
export interface ProductVariantCreatePricesProps { export interface ProductVariantCreatePricesProps {
attributes: ProductDetails_product_productType_variantAttributes[]; attributes: ProductDetails_product_productType_variantAttributes[];
data: ProductVariantCreateFormData; data: ProductVariantCreateFormData;
onValueClick: (id: string) => void;
onAttributeSelect: (id: string, type: PriceOrStock) => void;
onApplyPriceOrStockChange: (applyToAll: boolean, type: PriceOrStock) => void; onApplyPriceOrStockChange: (applyToAll: boolean, type: PriceOrStock) => void;
onApplyToAllChange: (value: string, type: PriceOrStock) => void; onApplyToAllChange: (value: string, type: PriceOrStock) => void;
onAttributeSelect: (id: string, type: PriceOrStock) => void;
onAttributeValueChange: (
id: string,
value: string,
type: PriceOrStock
) => void;
} }
const ProductVariantCreatePrices: React.FC< const ProductVariantCreatePrices: React.FC<
@ -47,31 +50,25 @@ const ProductVariantCreatePrices: React.FC<
data, data,
onApplyPriceOrStockChange, onApplyPriceOrStockChange,
onApplyToAllChange, onApplyToAllChange,
onAttributeSelect onAttributeSelect,
onAttributeValueChange
} = props; } = props;
const classes = useStyles(props); const classes = useStyles(props);
const intl = useIntl(); const intl = useIntl();
const selectedAttributes = attributes.filter(attribute => const attributeChoices = attributes.map(attribute => ({
isSelected(attribute.id, data.attributes, (a, b) => a === b)
);
const attributeChoices = selectedAttributes.map(attribute => ({
label: attribute.name, label: attribute.name,
value: attribute.id value: attribute.id
})); }));
const priceAttributeValues = data.price.all const priceAttributeValues = data.price.all
? null ? null
: data.price.attribute : data.price.attribute
? selectedAttributes.find( ? attributes.find(attribute => attribute.id === data.price.attribute).values
attribute => attribute.id === data.price.attribute
).values
: []; : [];
const stockAttributeValues = data.stock.all const stockAttributeValues = data.stock.all
? null ? null
: data.stock.attribute : data.stock.attribute
? selectedAttributes.find( ? attributes.find(attribute => attribute.id === data.stock.attribute).values
attribute => attribute.id === data.stock.attribute
).values
: []; : [];
return ( return (
@ -142,12 +139,13 @@ const ProductVariantCreatePrices: React.FC<
</div> </div>
</Grid> </Grid>
{priceAttributeValues && {priceAttributeValues &&
priceAttributeValues.map((attribute, attributeIndex) => ( priceAttributeValues.map(
(attributeValue, attributeValueIndex) => (
<> <>
<FormSpacer /> <FormSpacer />
<Grid variant="inverted"> <Grid variant="inverted">
<div className={classes.label}> <div className={classes.label}>
<Typography>{attribute.name}</Typography> <Typography>{attributeValue.name}</Typography>
</div> </div>
<div> <div>
<TextField <TextField
@ -157,12 +155,20 @@ const ProductVariantCreatePrices: React.FC<
id: "productVariantCreatePricesSetPricePlaceholder" id: "productVariantCreatePricesSetPricePlaceholder"
})} })}
fullWidth fullWidth
value={data.price.values[attributeIndex]} value={data.price.values[attributeValueIndex].value}
onChange={event =>
onAttributeValueChange(
attributeValue.id,
event.target.value,
"price"
)
}
/> />
</div> </div>
</Grid> </Grid>
</> </>
))} )
)}
</> </>
)} )}
</RadioGroup> </RadioGroup>
@ -233,12 +239,13 @@ const ProductVariantCreatePrices: React.FC<
</div> </div>
</Grid> </Grid>
{stockAttributeValues && {stockAttributeValues &&
stockAttributeValues.map((attribute, attributeIndex) => ( stockAttributeValues.map(
(attributeValue, attributeValueIndex) => (
<> <>
<FormSpacer /> <FormSpacer />
<Grid variant="inverted"> <Grid variant="inverted">
<div className={classes.label}> <div className={classes.label}>
<Typography>{attribute.name}</Typography> <Typography>{attributeValue.name}</Typography>
</div> </div>
<div> <div>
<TextField <TextField
@ -248,12 +255,20 @@ const ProductVariantCreatePrices: React.FC<
id: "productVariantCreatePricesSetStockPlaceholder" id: "productVariantCreatePricesSetStockPlaceholder"
})} })}
fullWidth fullWidth
value={data.stock.values[attributeIndex]} value={data.stock.values[attributeValueIndex].value}
onChange={event =>
onAttributeValueChange(
attributeValue.id,
event.target.value,
"stock"
)
}
/> />
</div> </div>
</Grid> </Grid>
</> </>
))} )
)}
</> </>
)} )}
</RadioGroup> </RadioGroup>

View file

@ -32,13 +32,13 @@ const useStyles = makeStyles((theme: Theme) => ({
width: "auto" width: "auto"
}, },
colPrice: { colPrice: {
width: 110 width: 160
}, },
colSku: { colSku: {
width: 110 width: 210
}, },
colStock: { colStock: {
width: 110 width: 160
}, },
hr: { hr: {
marginBottom: theme.spacing.unit, marginBottom: theme.spacing.unit,

View file

@ -14,7 +14,7 @@ import { ProductVariantCreateFormData } from "./form";
export interface ProductVariantCreateValuesProps { export interface ProductVariantCreateValuesProps {
attributes: ProductDetails_product_productType_variantAttributes[]; attributes: ProductDetails_product_productType_variantAttributes[];
data: ProductVariantCreateFormData; data: ProductVariantCreateFormData;
onValueClick: (id: string) => void; onValueClick: (attributeId: string, valueId: string) => void;
} }
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles((theme: Theme) => ({
@ -47,10 +47,16 @@ const ProductVariantCreateValues: React.FC<
<div className={classes.valueContainer}> <div className={classes.valueContainer}>
{attribute.values.map(value => ( {attribute.values.map(value => (
<ControlledCheckbox <ControlledCheckbox
checked={isSelected(value.id, data.values, (a, b) => a === b)} checked={isSelected(
value.id,
data.attributes.find(
dataAttribute => attribute.id === dataAttribute.id
).values,
(a, b) => a === b
)}
name={`value:${value.id}`} name={`value:${value.id}`}
label={value.name} label={value.name}
onChange={() => onValueClick(value.id)} onChange={() => onValueClick(attribute.id, value.id)}
/> />
))} ))}
</div> </div>

View file

@ -0,0 +1,457 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Reducer is able to select attribute values 1`] = `
Object {
"attributes": Array [
Object {
"id": "attr-1",
"values": Array [
"val-1-1",
"val-1-7",
],
},
Object {
"id": "attr-2",
"values": Array [
"val-2-2",
"val-2-4",
],
},
Object {
"id": "attr-4",
"values": Array [
"val-4-1",
"val-4-5",
],
},
],
"price": Object {
"all": true,
"attribute": undefined,
"value": "",
"values": Array [],
},
"stock": Object {
"all": true,
"attribute": undefined,
"value": "",
"values": Array [],
},
"variants": Array [],
}
`;
exports[`Reducer is able to select attributes 1`] = `
Object {
"attributes": Array [
Object {
"id": "attr-1",
"values": Array [],
},
Object {
"id": "attr-2",
"values": Array [],
},
Object {
"id": "attr-4",
"values": Array [],
},
],
"price": Object {
"all": true,
"attribute": undefined,
"value": "",
"values": Array [],
},
"stock": Object {
"all": true,
"attribute": undefined,
"value": "",
"values": Array [],
},
"variants": Array [],
}
`;
exports[`Reducer is able to select price for all variants 1`] = `
Object {
"attributes": Array [
Object {
"id": "attr-1",
"values": Array [
"val-1-1",
"val-1-7",
],
},
Object {
"id": "attr-2",
"values": Array [
"val-2-2",
"val-2-4",
],
},
Object {
"id": "attr-4",
"values": Array [
"val-4-1",
"val-4-5",
],
},
],
"price": Object {
"all": true,
"attribute": undefined,
"value": "45.99",
"values": Array [],
},
"stock": Object {
"all": true,
"attribute": undefined,
"value": "",
"values": Array [],
},
"variants": Array [],
}
`;
exports[`Reducer is able to select price to each attribute value 1`] = `
Object {
"attributes": Array [
Object {
"id": "attr-1",
"values": Array [
"val-1-1",
"val-1-7",
],
},
Object {
"id": "attr-2",
"values": Array [
"val-2-2",
"val-2-4",
],
},
Object {
"id": "attr-4",
"values": Array [
"val-4-1",
"val-4-5",
],
},
],
"price": Object {
"all": false,
"attribute": "attr-1",
"value": "",
"values": Array [
Object {
"id": "val-1-1",
"value": "45.99",
},
Object {
"id": "val-1-7",
"value": "51.99",
},
],
},
"stock": Object {
"all": true,
"attribute": undefined,
"value": "",
"values": Array [],
},
"variants": Array [],
}
`;
exports[`Reducer is able to select stock for all variants 1`] = `
Object {
"attributes": Array [
Object {
"id": "attr-1",
"values": Array [
"val-1-1",
"val-1-7",
],
},
Object {
"id": "attr-2",
"values": Array [
"val-2-2",
"val-2-4",
],
},
Object {
"id": "attr-4",
"values": Array [
"val-4-1",
"val-4-5",
],
},
],
"price": Object {
"all": true,
"attribute": undefined,
"value": "",
"values": Array [],
},
"stock": Object {
"all": true,
"attribute": undefined,
"value": "45.99",
"values": Array [],
},
"variants": Array [
Object {
"attributes": Array [
Object {
"id": "attr-1",
"values": Array [
"val-1-1",
],
},
Object {
"id": "attr-2",
"values": Array [
"val-2-2",
],
},
Object {
"id": "attr-4",
"values": Array [
"val-4-1",
],
},
],
"priceOverride": "",
"product": "",
"quantity": 45,
},
Object {
"attributes": Array [
Object {
"id": "attr-1",
"values": Array [
"val-1-1",
],
},
Object {
"id": "attr-2",
"values": Array [
"val-2-2",
],
},
Object {
"id": "attr-4",
"values": Array [
"val-4-5",
],
},
],
"priceOverride": "",
"product": "",
"quantity": 45,
},
Object {
"attributes": Array [
Object {
"id": "attr-1",
"values": Array [
"val-1-1",
],
},
Object {
"id": "attr-2",
"values": Array [
"val-2-4",
],
},
Object {
"id": "attr-4",
"values": Array [
"val-4-1",
],
},
],
"priceOverride": "",
"product": "",
"quantity": 45,
},
Object {
"attributes": Array [
Object {
"id": "attr-1",
"values": Array [
"val-1-1",
],
},
Object {
"id": "attr-2",
"values": Array [
"val-2-4",
],
},
Object {
"id": "attr-4",
"values": Array [
"val-4-5",
],
},
],
"priceOverride": "",
"product": "",
"quantity": 45,
},
Object {
"attributes": Array [
Object {
"id": "attr-1",
"values": Array [
"val-1-7",
],
},
Object {
"id": "attr-2",
"values": Array [
"val-2-2",
],
},
Object {
"id": "attr-4",
"values": Array [
"val-4-1",
],
},
],
"priceOverride": "",
"product": "",
"quantity": 45,
},
Object {
"attributes": Array [
Object {
"id": "attr-1",
"values": Array [
"val-1-7",
],
},
Object {
"id": "attr-2",
"values": Array [
"val-2-2",
],
},
Object {
"id": "attr-4",
"values": Array [
"val-4-5",
],
},
],
"priceOverride": "",
"product": "",
"quantity": 45,
},
Object {
"attributes": Array [
Object {
"id": "attr-1",
"values": Array [
"val-1-7",
],
},
Object {
"id": "attr-2",
"values": Array [
"val-2-4",
],
},
Object {
"id": "attr-4",
"values": Array [
"val-4-1",
],
},
],
"priceOverride": "",
"product": "",
"quantity": 45,
},
Object {
"attributes": Array [
Object {
"id": "attr-1",
"values": Array [
"val-1-7",
],
},
Object {
"id": "attr-2",
"values": Array [
"val-2-4",
],
},
Object {
"id": "attr-4",
"values": Array [
"val-4-5",
],
},
],
"priceOverride": "",
"product": "",
"quantity": 45,
},
],
}
`;
exports[`Reducer is able to select stock to each attribute value 1`] = `
Object {
"attributes": Array [
Object {
"id": "attr-1",
"values": Array [
"val-1-1",
"val-1-7",
],
},
Object {
"id": "attr-2",
"values": Array [
"val-2-2",
"val-2-4",
],
},
Object {
"id": "attr-4",
"values": Array [
"val-4-1",
"val-4-5",
],
},
],
"price": Object {
"all": true,
"attribute": undefined,
"value": "",
"values": Array [],
},
"stock": Object {
"all": false,
"attribute": "attr-1",
"value": "",
"values": Array [
Object {
"id": "val-1-1",
"value": "13",
},
Object {
"id": "val-1-7",
"value": "19",
},
],
},
"variants": Array [],
}
`;

View file

@ -0,0 +1,43 @@
import {
createVariantFlatMatrixDimension,
createVariants
} from "./createVariants";
import { thirdStep } from "./fixtures";
import { ProductVariantCreateFormData } from "./form";
describe("Creates variant matrix", () => {
it("with proper size", () => {
const attributes = thirdStep.attributes;
const matrix = createVariantFlatMatrixDimension([[]], attributes);
expect(matrix).toHaveLength(
attributes.reduce((acc, attribute) => acc * attribute.values.length, 1)
);
});
it("with constant price and stock", () => {
const price = "49.99";
const stock = 80;
const data: ProductVariantCreateFormData = {
...thirdStep,
price: {
...thirdStep.price,
all: true,
value: price
},
stock: {
...thirdStep.stock,
all: true,
value: stock.toString()
}
};
const variants = createVariants(data);
variants.forEach(variant => {
expect(variant.priceOverride).toBe(price);
expect(variant.quantity).toBe(stock);
});
});
});

View file

@ -0,0 +1,89 @@
import { ProductVariantCreateInput } from "@saleor/types/globalTypes";
import { Attribute, ProductVariantCreateFormData } from "./form";
interface CreateVariantAttributeValueInput {
attributeId: string;
attributeValueId: string;
}
type CreateVariantInput = CreateVariantAttributeValueInput[];
function createVariant(
data: ProductVariantCreateFormData,
attributes: CreateVariantInput
): ProductVariantCreateInput {
const priceOverride = data.price.all
? data.price.value
: data.price.values.find(
value =>
attributes.find(
attribute => attribute.attributeId === data.price.attribute
).attributeValueId === value.id
).value;
const quantity = parseInt(
data.stock.all
? data.stock.value
: data.stock.values.find(
value =>
attributes.find(
attribute => attribute.attributeId === data.stock.attribute
).attributeValueId === value.id
).value,
10
);
return {
attributes: attributes.map(attribute => ({
id: attribute.attributeId,
values: [attribute.attributeValueId]
})),
priceOverride,
product: "",
quantity
};
}
function addAttributeToVariant(
attribute: Attribute,
variant: CreateVariantInput
): CreateVariantInput[] {
return attribute.values.map(attributeValueId => [
...variant,
{
attributeId: attribute.id,
attributeValueId
}
]);
}
function addVariantAttributeInput(
data: CreateVariantInput[],
attribute: Attribute
): CreateVariantInput[] {
const variants = data
.map(variant => addAttributeToVariant(attribute, variant))
.reduce((acc, variantInput) => [...acc, ...variantInput]);
return variants;
}
export function createVariantFlatMatrixDimension(
variants: CreateVariantInput[],
attributes: Attribute[]
): CreateVariantInput[] {
if (attributes.length > 0) {
return createVariantFlatMatrixDimension(
addVariantAttributeInput(variants, attributes[0]),
attributes.slice(1)
);
} else {
return variants;
}
}
export function createVariants(
data: ProductVariantCreateFormData
): ProductVariantCreateInput[] {
const variants = createVariantFlatMatrixDimension([[]], data.attributes).map(
variant => createVariant(data, variant)
);
return variants;
}

View file

@ -0,0 +1,63 @@
import { initialForm, ProductVariantCreateFormData } from "./form";
export const attributes = [
{
id: "attr-1",
values: Array(9)
.fill(0)
.map((_, index) => `val-1-${index + 1}`)
},
{
id: "attr-2",
values: Array(6)
.fill(0)
.map((_, index) => `val-2-${index + 1}`)
},
{
id: "attr-3",
values: Array(4)
.fill(0)
.map((_, index) => `val-3-${index + 1}`)
},
{
id: "attr-4",
values: Array(11)
.fill(0)
.map((_, index) => `val-4-${index + 1}`)
}
];
export const secondStep: ProductVariantCreateFormData = {
...initialForm,
attributes: [
{
id: attributes[0].id,
values: []
},
{
id: attributes[1].id,
values: []
},
{
id: attributes[3].id,
values: []
}
]
};
export const thirdStep: ProductVariantCreateFormData = {
...secondStep,
attributes: [
{
id: attributes[0].id,
values: [0, 6].map(index => attributes[0].values[index])
},
{
id: attributes[1].id,
values: [1, 3].map(index => attributes[1].values[index])
},
{
id: attributes[3].id,
values: [0, 4].map(index => attributes[3].values[index])
}
]
};

View file

@ -1,16 +1,23 @@
import { ProductVariantCreateInput } from "../../../types/globalTypes"; import { ProductVariantCreateInput } from "../../../types/globalTypes";
export interface AttributeValue {
id: string;
value: string;
}
export interface AllOrAttribute { export interface AllOrAttribute {
all: boolean; all: boolean;
attribute: string; attribute: string;
value: string; value: string;
values: AttributeValue[];
}
export interface Attribute {
id: string;
values: string[]; values: string[];
} }
export interface ProductVariantCreateFormData { export interface ProductVariantCreateFormData {
attributes: string[]; attributes: Attribute[];
price: AllOrAttribute; price: AllOrAttribute;
stock: AllOrAttribute; stock: AllOrAttribute;
values: string[];
variants: ProductVariantCreateInput[]; variants: ProductVariantCreateInput[];
} }
@ -28,6 +35,5 @@ export const initialForm: ProductVariantCreateFormData = {
value: "", value: "",
values: [] values: []
}, },
values: [],
variants: [] variants: []
}; };

View file

@ -0,0 +1,171 @@
import { attributes, secondStep, thirdStep } from "./fixtures";
import { initialForm } from "./form";
import reducer from "./reducer";
function execActions<TState, TAction>(
initialState: TState,
reducer: (state: TState, action: TAction) => TState,
actions: TAction[]
): TState {
return actions.reduce((acc, action) => reducer(acc, action), initialState);
}
describe("Reducer is able to", () => {
it("select attributes", () => {
const state = execActions(initialForm, reducer, [
{
attributeId: attributes[0].id,
type: "selectAttribute"
},
{
attributeId: attributes[1].id,
type: "selectAttribute"
},
{
attributeId: attributes[3].id,
type: "selectAttribute"
}
]);
expect(state.attributes).toHaveLength(3);
expect(state).toMatchSnapshot();
});
it("select attribute values", () => {
const state = execActions(secondStep, reducer, [
{
attributeId: attributes[0].id,
type: "selectValue",
valueId: attributes[0].values[0]
},
{
attributeId: attributes[0].id,
type: "selectValue",
valueId: attributes[0].values[6]
},
{
attributeId: attributes[1].id,
type: "selectValue",
valueId: attributes[1].values[1]
},
{
attributeId: attributes[1].id,
type: "selectValue",
valueId: attributes[1].values[3]
},
{
attributeId: attributes[3].id,
type: "selectValue",
valueId: attributes[3].values[0]
},
{
attributeId: attributes[3].id,
type: "selectValue",
valueId: attributes[3].values[4]
}
]);
expect(state.attributes[0].values).toHaveLength(2);
expect(state.attributes[1].values).toHaveLength(2);
expect(state.attributes[2].values).toHaveLength(2);
expect(state).toMatchSnapshot();
});
it("select price for all variants", () => {
const value = "45.99";
const state = execActions(thirdStep, reducer, [
{
all: true,
type: "applyPriceToAll"
},
{
type: "changeApplyPriceToAllValue",
value
}
]);
expect(state.price.all).toBeTruthy();
expect(state.price.value).toBe(value);
expect(state).toMatchSnapshot();
});
it("select stock for all variants", () => {
const value = 45.99;
const state = execActions(thirdStep, reducer, [
{
all: true,
type: "applyStockToAll"
},
{
type: "changeApplyStockToAllValue",
value: value.toString()
}
]);
expect(state.stock.all).toBeTruthy();
expect(state.stock.value).toBe(value.toString());
expect(state).toMatchSnapshot();
});
it("select price to each attribute value", () => {
const value = 45.99;
const state = execActions(thirdStep, reducer, [
{
all: false,
type: "applyPriceToAll"
},
{
attributeId: attributes[0].id,
type: "changeApplyPriceToAttributeId"
},
{
type: "changeAttributeValuePrice",
value: value.toString(),
valueId: attributes[0].values[0]
},
{
type: "changeAttributeValuePrice",
value: (value + 6).toString(),
valueId: attributes[0].values[6]
}
]);
expect(state.price.all).toBeFalsy();
expect(state.price.values).toHaveLength(
state.attributes.find(attribute => state.price.attribute === attribute.id)
.values.length
);
expect(state).toMatchSnapshot();
});
it("select stock to each attribute value", () => {
const value = 13;
const state = execActions(thirdStep, reducer, [
{
all: false,
type: "applyStockToAll"
},
{
attributeId: attributes[0].id,
type: "changeApplyStockToAttributeId"
},
{
type: "changeAttributeValueStock",
value: value.toString(),
valueId: attributes[0].values[0]
},
{
type: "changeAttributeValueStock",
value: (value + 6).toString(),
valueId: attributes[0].values[6]
}
]);
expect(state.stock.all).toBeFalsy();
expect(state.stock.values).toHaveLength(
state.attributes.find(attribute => state.stock.attribute === attribute.id)
.values.length
);
expect(state).toMatchSnapshot();
});
});

View file

@ -1,4 +1,5 @@
import { toggle, updateAtIndex } from "@saleor/utils/lists"; import { add, remove, toggle, updateAtIndex } from "@saleor/utils/lists";
import { createVariants } from "./createVariants";
import { initialForm, ProductVariantCreateFormData } from "./form"; import { initialForm, ProductVariantCreateFormData } from "./form";
export type ProductVariantCreateReducerActionType = export type ProductVariantCreateReducerActionType =
@ -10,22 +11,30 @@ export type ProductVariantCreateReducerActionType =
| "changeApplyPriceToAttributeId" | "changeApplyPriceToAttributeId"
| "changeApplyStockToAllValue" | "changeApplyStockToAllValue"
| "changeApplyStockToAttributeId" | "changeApplyStockToAttributeId"
| "changeAttributePrice" | "changeAttributeValuePrice"
| "changeAttributeStock" | "changeAttributeValueStock"
| "selectAttribute" | "selectAttribute"
| "selectValue"; | "selectValue";
export interface ProductVariantCreateReducerAction { export interface ProductVariantCreateReducerAction {
all?: boolean; all?: boolean;
id?: string; attributeId?: string;
type: ProductVariantCreateReducerActionType; type: ProductVariantCreateReducerActionType;
value?: string; value?: string;
valueId?: string;
} }
function selectAttribute( function selectAttribute(
state: ProductVariantCreateFormData, state: ProductVariantCreateFormData,
attribute: string attributeId: string
): ProductVariantCreateFormData { ): ProductVariantCreateFormData {
const attributes = toggle(attribute, state.attributes, (a, b) => a === b); const attributes = toggle(
{
id: attributeId,
values: []
},
state.attributes,
(a, b) => a.id === b.id
);
return { return {
...initialForm, ...initialForm,
@ -35,14 +44,24 @@ function selectAttribute(
function selectValue( function selectValue(
state: ProductVariantCreateFormData, state: ProductVariantCreateFormData,
value: string attributeId: string,
valueId: string
): ProductVariantCreateFormData { ): ProductVariantCreateFormData {
const values = toggle(value, state.values, (a, b) => a === b); const attribute = state.attributes.find(
attribute => attribute.id === attributeId
);
const values = toggle(valueId, attribute.values, (a, b) => a === b);
const updatedAttributes = add(
{
id: attributeId,
values
},
remove(attribute, state.attributes, (a, b) => a.id === b.id)
);
return { return {
...initialForm, ...initialForm,
attributes: state.attributes, attributes: updatedAttributes
values
}; };
} }
@ -72,13 +91,27 @@ function applyStockToAll(
}; };
} }
function changeAttributePrice( function changeAttributeValuePrice(
state: ProductVariantCreateFormData, state: ProductVariantCreateFormData,
attribute: string, attributeValueId: string,
price: string price: string
): ProductVariantCreateFormData { ): ProductVariantCreateFormData {
const index = state.price.values.indexOf(attribute); const index = state.price.values.findIndex(
const values = updateAtIndex(price, state.price.values, index); value => value.id === attributeValueId
);
if (index === -1) {
throw new Error(`Value with id ${attributeValueId} not found`);
}
const values = updateAtIndex(
{
id: attributeValueId,
value: price
},
state.price.values,
index
);
return { return {
...state, ...state,
@ -89,13 +122,27 @@ function changeAttributePrice(
}; };
} }
function changeAttributeStock( function changeAttributeValueStock(
state: ProductVariantCreateFormData, state: ProductVariantCreateFormData,
attribute: string, attributeValueId: string,
stock: string stock: string
): ProductVariantCreateFormData { ): ProductVariantCreateFormData {
const index = state.stock.values.indexOf(attribute); const index = state.stock.values.findIndex(
const values = updateAtIndex(stock, state.stock.values, index); value => value.id === attributeValueId
);
if (index === -1) {
throw new Error(`Value with id ${attributeValueId} not found`);
}
const values = updateAtIndex(
{
id: attributeValueId,
value: stock
},
state.stock.values,
index
);
return { return {
...state, ...state,
@ -108,13 +155,22 @@ function changeAttributeStock(
function changeApplyPriceToAttributeId( function changeApplyPriceToAttributeId(
state: ProductVariantCreateFormData, state: ProductVariantCreateFormData,
attribute: string attributeId: string
): ProductVariantCreateFormData { ): ProductVariantCreateFormData {
const attribute = state.attributes.find(
attribute => attribute.id === attributeId
);
const values = attribute.values.map(id => ({
id,
value: ""
}));
return { return {
...state, ...state,
price: { price: {
...state.price, ...state.price,
attribute attribute: attributeId,
values
} }
}; };
} }
@ -127,7 +183,13 @@ function changeApplyStockToAttributeId(
...state, ...state,
stock: { stock: {
...state.stock, ...state.stock,
attribute attribute,
values: state.attributes
.find(stateAttribute => stateAttribute.id === attribute)
.values.map(attributeValue => ({
id: attributeValue,
value: ""
}))
} }
}; };
} }
@ -149,13 +211,18 @@ function changeApplyStockToAllValue(
state: ProductVariantCreateFormData, state: ProductVariantCreateFormData,
value: string value: string
): ProductVariantCreateFormData { ): ProductVariantCreateFormData {
return { const data = {
...state, ...state,
stock: { stock: {
...state.stock, ...state.stock,
value value
} }
}; };
return {
...data,
variants: createVariants(data)
};
} }
function reduceProductVariantCreateFormData( function reduceProductVariantCreateFormData(
@ -164,29 +231,30 @@ function reduceProductVariantCreateFormData(
) { ) {
switch (action.type) { switch (action.type) {
case "selectAttribute": case "selectAttribute":
return selectAttribute(prevState, action.id); return selectAttribute(prevState, action.attributeId);
case "selectValue": case "selectValue":
return selectValue(prevState, action.id); return selectValue(prevState, action.attributeId, action.valueId);
case "applyPriceToAll": case "applyPriceToAll":
return applyPriceToAll(prevState, action.all); return applyPriceToAll(prevState, action.all);
case "applyStockToAll": case "applyStockToAll":
return applyStockToAll(prevState, action.all); return applyStockToAll(prevState, action.all);
case "changeAttributePrice": case "changeAttributeValuePrice":
return changeAttributePrice(prevState, action.id, action.value); return changeAttributeValuePrice(prevState, action.valueId, action.value);
case "changeAttributeStock": case "changeAttributeValueStock":
return changeAttributeStock(prevState, action.id, action.value); return changeAttributeValueStock(prevState, action.valueId, action.value);
case "changeApplyPriceToAttributeId": case "changeApplyPriceToAttributeId":
return changeApplyPriceToAttributeId(prevState, action.id); return changeApplyPriceToAttributeId(prevState, action.attributeId);
case "changeApplyStockToAttributeId": case "changeApplyStockToAttributeId":
return changeApplyStockToAttributeId(prevState, action.id); return changeApplyStockToAttributeId(prevState, action.attributeId);
case "changeApplyPriceToAllValue": case "changeApplyPriceToAllValue":
return changeApplyPriceToAllValue(prevState, action.value); return changeApplyPriceToAllValue(prevState, action.value);
case "changeApplyStockToAllValue": case "changeApplyStockToAllValue":
return changeApplyStockToAllValue(prevState, action.value); return changeApplyStockToAllValue(prevState, action.value);
} default:
return prevState; return prevState;
}
} }
export default reduceProductVariantCreateFormData; export default reduceProductVariantCreateFormData;