Make it work

This commit is contained in:
dominik-zeglen 2019-09-24 14:27:11 +02:00
parent 4d9171faa4
commit fcfdd432b4
7 changed files with 488 additions and 158 deletions

View file

@ -112,66 +112,64 @@ const ProductVariantCreatePrices: React.FC<
})} })}
onChange={() => onApplyPriceOrStockChange(false, "price")} onChange={() => onApplyPriceOrStockChange(false, "price")}
/> />
{!data.price.all && (
<>
<FormSpacer />
<Grid variant="inverted">
<div className={classes.label}>
<Typography>
<FormattedMessage
defaultMessage="Choose attribute"
description="variant attribute"
/>
</Typography>
</div>
<div>
<SingleSelectField
choices={attributeChoices}
label={intl.formatMessage({
defaultMessage: "Attribute",
description: "variant attribute"
})}
value={data.price.attribute}
onChange={event =>
onAttributeSelect(event.target.value, "price")
}
/>
</div>
</Grid>
{priceAttributeValues &&
priceAttributeValues.map(
(attributeValue, attributeValueIndex) => (
<>
<FormSpacer />
<Grid variant="inverted">
<div className={classes.label}>
<Typography>{attributeValue.name}</Typography>
</div>
<div>
<TextField
label={intl.formatMessage({
defaultMessage: "Price",
description: "variant price",
id: "productVariantCreatePricesSetPricePlaceholder"
})}
fullWidth
value={data.price.values[attributeValueIndex].value}
onChange={event =>
onAttributeValueChange(
attributeValue.id,
event.target.value,
"price"
)
}
/>
</div>
</Grid>
</>
)
)}
</>
)}
</RadioGroup> </RadioGroup>
{!data.price.all && (
<>
<FormSpacer />
<Grid variant="inverted">
<div className={classes.label}>
<Typography>
<FormattedMessage
defaultMessage="Choose attribute"
description="variant attribute"
/>
</Typography>
</div>
<div>
<SingleSelectField
choices={attributeChoices}
label={intl.formatMessage({
defaultMessage: "Attribute",
description: "variant attribute"
})}
value={data.price.attribute}
onChange={event =>
onAttributeSelect(event.target.value, "price")
}
/>
</div>
</Grid>
{priceAttributeValues &&
priceAttributeValues.map((attributeValue, attributeValueIndex) => (
<>
<FormSpacer />
<Grid variant="inverted">
<div className={classes.label}>
<Typography>{attributeValue.name}</Typography>
</div>
<div>
<TextField
label={intl.formatMessage({
defaultMessage: "Price",
description: "variant price",
id: "productVariantCreatePricesSetPricePlaceholder"
})}
fullWidth
value={data.price.values[attributeValueIndex].value}
onChange={event =>
onAttributeValueChange(
attributeValue.id,
event.target.value,
"price"
)
}
/>
</div>
</Grid>
</>
))}
</>
)}
<FormSpacer /> <FormSpacer />
<Typography color="textSecondary" variant="headline"> <Typography color="textSecondary" variant="headline">
<FormattedMessage <FormattedMessage
@ -212,66 +210,64 @@ const ProductVariantCreatePrices: React.FC<
})} })}
onChange={() => onApplyPriceOrStockChange(false, "stock")} onChange={() => onApplyPriceOrStockChange(false, "stock")}
/> />
{!data.stock.all && (
<>
<FormSpacer />
<Grid variant="inverted">
<div className={classes.label}>
<Typography>
<FormattedMessage
defaultMessage="Choose attribute"
description="variant attribute"
/>
</Typography>
</div>
<div>
<SingleSelectField
choices={attributeChoices}
label={intl.formatMessage({
defaultMessage: "Attribute",
description: "variant attribute"
})}
value={data.stock.attribute}
onChange={event =>
onAttributeSelect(event.target.value, "stock")
}
/>
</div>
</Grid>
{stockAttributeValues &&
stockAttributeValues.map(
(attributeValue, attributeValueIndex) => (
<>
<FormSpacer />
<Grid variant="inverted">
<div className={classes.label}>
<Typography>{attributeValue.name}</Typography>
</div>
<div>
<TextField
label={intl.formatMessage({
defaultMessage: "Stock",
description: "variant stock",
id: "productVariantCreatePricesSetStockPlaceholder"
})}
fullWidth
value={data.stock.values[attributeValueIndex].value}
onChange={event =>
onAttributeValueChange(
attributeValue.id,
event.target.value,
"stock"
)
}
/>
</div>
</Grid>
</>
)
)}
</>
)}
</RadioGroup> </RadioGroup>
{!data.stock.all && (
<>
<FormSpacer />
<Grid variant="inverted">
<div className={classes.label}>
<Typography>
<FormattedMessage
defaultMessage="Choose attribute"
description="variant attribute"
/>
</Typography>
</div>
<div>
<SingleSelectField
choices={attributeChoices}
label={intl.formatMessage({
defaultMessage: "Attribute",
description: "variant attribute"
})}
value={data.stock.attribute}
onChange={event =>
onAttributeSelect(event.target.value, "stock")
}
/>
</div>
</Grid>
{stockAttributeValues &&
stockAttributeValues.map((attributeValue, attributeValueIndex) => (
<>
<FormSpacer />
<Grid variant="inverted">
<div className={classes.label}>
<Typography>{attributeValue.name}</Typography>
</div>
<div>
<TextField
label={intl.formatMessage({
defaultMessage: "Stock",
description: "variant stock",
id: "productVariantCreatePricesSetStockPlaceholder"
})}
fullWidth
value={data.stock.values[attributeValueIndex].value}
onChange={event =>
onAttributeValueChange(
attributeValue.id,
event.target.value,
"stock"
)
}
/>
</div>
</Grid>
</>
))}
</>
)}
</> </>
); );
}; };

View file

@ -125,7 +125,9 @@ const ProductVariantCreateSummary: React.FC<
<TableBody> <TableBody>
{data.variants.map(variant => ( {data.variants.map(variant => (
<TableRow <TableRow
key={variant.attributes.map(attribute => attribute.id).join(":")} key={variant.attributes
.map(attribute => attribute.values[0])
.join(":")}
> >
<TableCell className={classNames(classes.col, classes.colName)}> <TableCell className={classNames(classes.col, classes.colName)}>
{getVariantName(variant, attributes).map( {getVariantName(variant, attributes).map(

View file

@ -2,7 +2,7 @@ import {
createVariantFlatMatrixDimension, createVariantFlatMatrixDimension,
createVariants createVariants
} from "./createVariants"; } from "./createVariants";
import { thirdStep } from "./fixtures"; import { attributes, thirdStep } from "./fixtures";
import { ProductVariantCreateFormData } from "./form"; import { ProductVariantCreateFormData } from "./form";
describe("Creates variant matrix", () => { describe("Creates variant matrix", () => {
@ -35,9 +35,186 @@ describe("Creates variant matrix", () => {
}; };
const variants = createVariants(data); const variants = createVariants(data);
expect(variants).toHaveLength(
thirdStep.attributes.reduce(
(acc, attribute) => acc * attribute.values.length,
1
)
);
variants.forEach(variant => { variants.forEach(variant => {
expect(variant.priceOverride).toBe(price); expect(variant.priceOverride).toBe(price);
expect(variant.quantity).toBe(stock); expect(variant.quantity).toBe(stock);
}); });
}); });
it("with constant stock and attribute dependent price", () => {
const price = 49.99;
const stock = 80;
const attribute = attributes.find(
attribute => attribute.id === thirdStep.attributes[0].id
);
const data: ProductVariantCreateFormData = {
...thirdStep,
price: {
...thirdStep.price,
all: false,
attribute: attribute.id,
values: attribute.values.map((attributeValue, attributeValueIndex) => ({
id: attributeValue,
value: (price * (attributeValueIndex + 1)).toString()
}))
},
stock: {
...thirdStep.stock,
all: true,
value: stock.toString()
}
};
const variants = createVariants(data);
expect(variants).toHaveLength(
thirdStep.attributes.reduce(
(acc, attribute) => acc * attribute.values.length,
1
)
);
variants.forEach(variant => {
expect(variant.quantity).toBe(stock);
});
attribute.values.forEach((attributeValue, attributeValueIndex) => {
variants
.filter(
variant =>
variant.attributes.find(
variantAttribute => variantAttribute.id === attribute.id
).values[0] === attributeValue
)
.forEach(variant => {
expect(variant.priceOverride).toBe(
(price * (attributeValueIndex + 1)).toString()
);
});
});
});
it("with constant price and attribute dependent stock", () => {
const price = "49.99";
const stock = 80;
const attribute = attributes.find(
attribute => attribute.id === thirdStep.attributes[0].id
);
const data: ProductVariantCreateFormData = {
...thirdStep,
price: {
...thirdStep.price,
all: true,
value: price
},
stock: {
...thirdStep.stock,
all: false,
attribute: attribute.id,
values: attribute.values.map((attributeValue, attributeValueIndex) => ({
id: attributeValue,
value: (stock * (attributeValueIndex + 1)).toString()
}))
}
};
const variants = createVariants(data);
expect(variants).toHaveLength(
thirdStep.attributes.reduce(
(acc, attribute) => acc * attribute.values.length,
1
)
);
variants.forEach(variant => {
expect(variant.priceOverride).toBe(price);
});
attribute.values.forEach((attributeValue, attributeValueIndex) => {
variants
.filter(
variant =>
variant.attributes.find(
variantAttribute => variantAttribute.id === attribute.id
).values[0] === attributeValue
)
.forEach(variant => {
expect(variant.quantity).toBe(stock * (attributeValueIndex + 1));
});
});
});
it("with attribute dependent price and stock", () => {
const price = 49.99;
const stock = 80;
const attribute = attributes.find(
attribute => attribute.id === thirdStep.attributes[0].id
);
const data: ProductVariantCreateFormData = {
...thirdStep,
price: {
...thirdStep.price,
all: false,
attribute: attribute.id,
values: attribute.values.map((attributeValue, attributeValueIndex) => ({
id: attributeValue,
value: (price * (attributeValueIndex + 1)).toString()
}))
},
stock: {
...thirdStep.stock,
all: false,
attribute: attribute.id,
values: attribute.values.map((attributeValue, attributeValueIndex) => ({
id: attributeValue,
value: (stock * (attributeValueIndex + 1)).toString()
}))
}
};
const variants = createVariants(data);
expect(variants).toHaveLength(
thirdStep.attributes.reduce(
(acc, attribute) => acc * attribute.values.length,
1
)
);
attribute.values.forEach((attributeValue, attributeValueIndex) => {
variants
.filter(
variant =>
variant.attributes.find(
variantAttribute => variantAttribute.id === attribute.id
).values[0] === attributeValue
)
.forEach(variant => {
expect(variant.priceOverride).toBe(
(price * (attributeValueIndex + 1)).toString()
);
});
});
attribute.values.forEach((attributeValue, attributeValueIndex) => {
variants
.filter(
variant =>
variant.attributes.find(
variantAttribute => variantAttribute.id === attribute.id
).values[0] === attributeValue
)
.forEach(variant => {
expect(variant.quantity).toBe(stock * (attributeValueIndex + 1));
});
});
});
}); });

View file

@ -1,32 +1,42 @@
import { ProductVariantCreateInput } from "@saleor/types/globalTypes"; import { ProductVariantCreateInput } from "@saleor/types/globalTypes";
import { Attribute, ProductVariantCreateFormData } from "./form"; import {
AllOrAttribute,
Attribute,
ProductVariantCreateFormData
} from "./form";
interface CreateVariantAttributeValueInput { interface CreateVariantAttributeValueInput {
attributeId: string; attributeId: string;
attributeValueId: string; attributeValueId: string;
} }
type CreateVariantInput = CreateVariantAttributeValueInput[]; type CreateVariantInput = CreateVariantAttributeValueInput[];
function getAttributeValuePriceOrStock(
attributes: CreateVariantInput,
priceOrStock: AllOrAttribute
): string {
const attribute = attributes.find(
attribute => attribute.attributeId === priceOrStock.attribute
);
const attributeValue = priceOrStock.values.find(
attributeValue => attribute.attributeValueId === attributeValue.id
);
return attributeValue.value;
}
function createVariant( function createVariant(
data: ProductVariantCreateFormData, data: ProductVariantCreateFormData,
attributes: CreateVariantInput attributes: CreateVariantInput
): ProductVariantCreateInput { ): ProductVariantCreateInput {
const priceOverride = data.price.all const priceOverride = data.price.all
? data.price.value ? data.price.value
: data.price.values.find( : getAttributeValuePriceOrStock(attributes, data.price);
value =>
attributes.find(
attribute => attribute.attributeId === data.price.attribute
).attributeValueId === value.id
).value;
const quantity = parseInt( const quantity = parseInt(
data.stock.all data.stock.all
? data.stock.value ? data.stock.value
: data.stock.values.find( : getAttributeValuePriceOrStock(attributes, data.stock),
value =>
attributes.find(
attribute => attribute.attributeId === data.stock.attribute
).attributeValueId === value.id
).value,
10 10
); );
@ -81,6 +91,12 @@ export function createVariantFlatMatrixDimension(
export function createVariants( export function createVariants(
data: ProductVariantCreateFormData data: ProductVariantCreateFormData
): ProductVariantCreateInput[] { ): ProductVariantCreateInput[] {
if (
(!data.price.all && !data.price.attribute) ||
(!data.stock.all && !data.stock.attribute)
) {
return [];
}
const variants = createVariantFlatMatrixDimension([[]], data.attributes).map( const variants = createVariantFlatMatrixDimension([[]], data.attributes).map(
variant => createVariant(data, variant) variant => createVariant(data, variant)
); );

View file

@ -1,4 +1,9 @@
import { initialForm, ProductVariantCreateFormData } from "./form"; import { createVariants } from "./createVariants";
import {
AllOrAttribute,
initialForm,
ProductVariantCreateFormData
} from "./form";
export const attributes = [ export const attributes = [
{ {
@ -44,6 +49,7 @@ export const secondStep: ProductVariantCreateFormData = {
} }
] ]
}; };
export const thirdStep: ProductVariantCreateFormData = { export const thirdStep: ProductVariantCreateFormData = {
...secondStep, ...secondStep,
attributes: [ attributes: [
@ -61,3 +67,44 @@ export const thirdStep: ProductVariantCreateFormData = {
} }
] ]
}; };
const price: AllOrAttribute = {
all: false,
attribute: thirdStep.attributes[1].id,
value: "",
values: [
{
id: thirdStep.attributes[1].values[0],
value: "24.99"
},
{
id: thirdStep.attributes[1].values[1],
value: "26.99"
}
]
};
const stock: AllOrAttribute = {
all: false,
attribute: thirdStep.attributes[2].id,
value: "",
values: [
{
id: thirdStep.attributes[2].values[0],
value: "50"
},
{
id: thirdStep.attributes[2].values[1],
value: "35"
}
]
};
export const fourthStep: ProductVariantCreateFormData = {
...thirdStep,
price,
stock,
variants: createVariants({
...thirdStep,
price,
stock
})
};

View file

@ -1,6 +1,6 @@
import { attributes, secondStep, thirdStep } from "./fixtures"; import { attributes, fourthStep, secondStep, thirdStep } from "./fixtures";
import { initialForm } from "./form"; import { initialForm } from "./form";
import reducer from "./reducer"; import reducer, { VariantField } from "./reducer";
function execActions<TState, TAction>( function execActions<TState, TAction>(
initialState: TState, initialState: TState,
@ -108,6 +108,7 @@ describe("Reducer is able to", () => {
}); });
it("select price to each attribute value", () => { it("select price to each attribute value", () => {
const attribute = thirdStep.attributes[0];
const value = 45.99; const value = 45.99;
const state = execActions(thirdStep, reducer, [ const state = execActions(thirdStep, reducer, [
{ {
@ -115,18 +116,18 @@ describe("Reducer is able to", () => {
type: "applyPriceToAll" type: "applyPriceToAll"
}, },
{ {
attributeId: attributes[0].id, attributeId: attribute.id,
type: "changeApplyPriceToAttributeId" type: "changeApplyPriceToAttributeId"
}, },
{ {
type: "changeAttributeValuePrice", type: "changeAttributeValuePrice",
value: value.toString(), value: value.toString(),
valueId: attributes[0].values[0] valueId: attribute.values[0]
}, },
{ {
type: "changeAttributeValuePrice", type: "changeAttributeValuePrice",
value: (value + 6).toString(), value: (value + 6).toString(),
valueId: attributes[0].values[6] valueId: attribute.values[1]
} }
]); ]);
@ -139,6 +140,7 @@ describe("Reducer is able to", () => {
}); });
it("select stock to each attribute value", () => { it("select stock to each attribute value", () => {
const attribute = thirdStep.attributes[0];
const value = 13; const value = 13;
const state = execActions(thirdStep, reducer, [ const state = execActions(thirdStep, reducer, [
{ {
@ -146,18 +148,18 @@ describe("Reducer is able to", () => {
type: "applyStockToAll" type: "applyStockToAll"
}, },
{ {
attributeId: attributes[0].id, attributeId: attribute.id,
type: "changeApplyStockToAttributeId" type: "changeApplyStockToAttributeId"
}, },
{ {
type: "changeAttributeValueStock", type: "changeAttributeValueStock",
value: value.toString(), value: value.toString(),
valueId: attributes[0].values[0] valueId: attribute.values[0]
}, },
{ {
type: "changeAttributeValueStock", type: "changeAttributeValueStock",
value: (value + 6).toString(), value: (value + 6).toString(),
valueId: attributes[0].values[6] valueId: attribute.values[1]
} }
]); ]);
@ -168,4 +170,24 @@ describe("Reducer is able to", () => {
); );
expect(state).toMatchSnapshot(); expect(state).toMatchSnapshot();
}); });
it("modify individual variant price", () => {
const field: VariantField = "price";
const value = "49.99";
const variantIndex = 3;
const state = execActions(fourthStep, reducer, [
{
field,
type: "changeVariantData",
value,
variantIndex
}
]);
expect(state.variants[variantIndex].priceOverride).toBe(value);
expect(state.variants[variantIndex - 1].priceOverride).toBe(
fourthStep.variants[variantIndex - 1].priceOverride
);
});
}); });

View file

@ -13,14 +13,19 @@ export type ProductVariantCreateReducerActionType =
| "changeApplyStockToAttributeId" | "changeApplyStockToAttributeId"
| "changeAttributeValuePrice" | "changeAttributeValuePrice"
| "changeAttributeValueStock" | "changeAttributeValueStock"
| "changeVariantData"
| "selectAttribute" | "selectAttribute"
| "selectValue"; | "selectValue";
export type VariantField = "stock" | "price" | "sku";
export interface ProductVariantCreateReducerAction { export interface ProductVariantCreateReducerAction {
all?: boolean; all?: boolean;
attributeId?: string; attributeId?: string;
field?: VariantField;
type: ProductVariantCreateReducerActionType; type: ProductVariantCreateReducerActionType;
value?: string; value?: string;
valueId?: string; valueId?: string;
variantIndex?: number;
} }
function selectAttribute( function selectAttribute(
@ -69,26 +74,36 @@ function applyPriceToAll(
state: ProductVariantCreateFormData, state: ProductVariantCreateFormData,
value: boolean value: boolean
): ProductVariantCreateFormData { ): ProductVariantCreateFormData {
return { const data = {
...state, ...state,
price: { price: {
...state.price, ...state.price,
all: value all: value
} }
}; };
return {
...data,
variants: createVariants(data)
};
} }
function applyStockToAll( function applyStockToAll(
state: ProductVariantCreateFormData, state: ProductVariantCreateFormData,
value: boolean value: boolean
): ProductVariantCreateFormData { ): ProductVariantCreateFormData {
return { const data = {
...state, ...state,
stock: { stock: {
...state.stock, ...state.stock,
all: value all: value
} }
}; };
return {
...data,
variants: createVariants(data)
};
} }
function changeAttributeValuePrice( function changeAttributeValuePrice(
@ -113,13 +128,18 @@ function changeAttributeValuePrice(
index index
); );
return { const data = {
...state, ...state,
price: { price: {
...state.price, ...state.price,
values values
} }
}; };
return {
...data,
variants: createVariants(data)
};
} }
function changeAttributeValueStock( function changeAttributeValueStock(
@ -144,13 +164,18 @@ function changeAttributeValueStock(
index index
); );
return { const data = {
...state, ...state,
stock: { stock: {
...state.stock, ...state.stock,
values values
} }
}; };
return {
...data,
variants: createVariants(data)
};
} }
function changeApplyPriceToAttributeId( function changeApplyPriceToAttributeId(
@ -164,8 +189,7 @@ function changeApplyPriceToAttributeId(
id, id,
value: "" value: ""
})); }));
const data = {
return {
...state, ...state,
price: { price: {
...state.price, ...state.price,
@ -173,38 +197,56 @@ function changeApplyPriceToAttributeId(
values values
} }
}; };
return {
...data,
variants: createVariants(data)
};
} }
function changeApplyStockToAttributeId( function changeApplyStockToAttributeId(
state: ProductVariantCreateFormData, state: ProductVariantCreateFormData,
attribute: string attributeId: string
): ProductVariantCreateFormData { ): ProductVariantCreateFormData {
return { const attribute = state.attributes.find(
attribute => attribute.id === attributeId
);
const values = attribute.values.map(id => ({
id,
value: ""
}));
const data = {
...state, ...state,
stock: { stock: {
...state.stock, ...state.stock,
attribute, attribute: attributeId,
values: state.attributes values
.find(stateAttribute => stateAttribute.id === attribute)
.values.map(attributeValue => ({
id: attributeValue,
value: ""
}))
} }
}; };
return {
...data,
variants: createVariants(data)
};
} }
function changeApplyPriceToAllValue( function changeApplyPriceToAllValue(
state: ProductVariantCreateFormData, state: ProductVariantCreateFormData,
value: string value: string
): ProductVariantCreateFormData { ): ProductVariantCreateFormData {
return { const data = {
...state, ...state,
price: { price: {
...state.price, ...state.price,
value value
} }
}; };
return {
...data,
variants: createVariants(data)
};
} }
function changeApplyStockToAllValue( function changeApplyStockToAllValue(
@ -225,6 +267,27 @@ function changeApplyStockToAllValue(
}; };
} }
function changeVariantData(
state: ProductVariantCreateFormData,
field: VariantField,
value: string,
variantIndex: number
): ProductVariantCreateFormData {
const variant = state.variants[variantIndex];
if (field === "price") {
variant.priceOverride = value;
} else if (field === "sku") {
variant.sku = value;
} else {
variant.quantity = parseInt(value, 10);
}
return {
...state,
variants: updateAtIndex(variant, state.variants, variantIndex)
};
}
function reduceProductVariantCreateFormData( function reduceProductVariantCreateFormData(
prevState: ProductVariantCreateFormData, prevState: ProductVariantCreateFormData,
action: ProductVariantCreateReducerAction action: ProductVariantCreateReducerAction
@ -252,6 +315,13 @@ function reduceProductVariantCreateFormData(
return changeApplyPriceToAllValue(prevState, action.value); return changeApplyPriceToAllValue(prevState, action.value);
case "changeApplyStockToAllValue": case "changeApplyStockToAllValue":
return changeApplyStockToAllValue(prevState, action.value); return changeApplyStockToAllValue(prevState, action.value);
case "changeVariantData":
return changeVariantData(
prevState,
action.field,
action.value,
action.variantIndex
);
default: default:
return prevState; return prevState;
} }