diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx
index b81f4a54b..6a1be87ec 100644
--- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx
+++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx
@@ -112,66 +112,64 @@ const ProductVariantCreatePrices: React.FC<
})}
onChange={() => onApplyPriceOrStockChange(false, "price")}
/>
- {!data.price.all && (
- <>
-
-
-
-
-
-
-
-
-
- onAttributeSelect(event.target.value, "price")
- }
- />
-
-
- {priceAttributeValues &&
- priceAttributeValues.map(
- (attributeValue, attributeValueIndex) => (
- <>
-
-
-
- {attributeValue.name}
-
-
-
- onAttributeValueChange(
- attributeValue.id,
- event.target.value,
- "price"
- )
- }
- />
-
-
- >
- )
- )}
- >
- )}
+ {!data.price.all && (
+ <>
+
+
+
+
+
+
+
+
+
+ onAttributeSelect(event.target.value, "price")
+ }
+ />
+
+
+ {priceAttributeValues &&
+ priceAttributeValues.map((attributeValue, attributeValueIndex) => (
+ <>
+
+
+
+ {attributeValue.name}
+
+
+
+ onAttributeValueChange(
+ attributeValue.id,
+ event.target.value,
+ "price"
+ )
+ }
+ />
+
+
+ >
+ ))}
+ >
+ )}
onApplyPriceOrStockChange(false, "stock")}
/>
- {!data.stock.all && (
- <>
-
-
-
-
-
-
-
-
-
- onAttributeSelect(event.target.value, "stock")
- }
- />
-
-
- {stockAttributeValues &&
- stockAttributeValues.map(
- (attributeValue, attributeValueIndex) => (
- <>
-
-
-
- {attributeValue.name}
-
-
-
- onAttributeValueChange(
- attributeValue.id,
- event.target.value,
- "stock"
- )
- }
- />
-
-
- >
- )
- )}
- >
- )}
+ {!data.stock.all && (
+ <>
+
+
+
+
+
+
+
+
+
+ onAttributeSelect(event.target.value, "stock")
+ }
+ />
+
+
+ {stockAttributeValues &&
+ stockAttributeValues.map((attributeValue, attributeValueIndex) => (
+ <>
+
+
+
+ {attributeValue.name}
+
+
+
+ onAttributeValueChange(
+ attributeValue.id,
+ event.target.value,
+ "stock"
+ )
+ }
+ />
+
+
+ >
+ ))}
+ >
+ )}
>
);
};
diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx
index 4d695d5c6..62ef027e2 100644
--- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx
+++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx
@@ -125,7 +125,9 @@ const ProductVariantCreateSummary: React.FC<
{data.variants.map(variant => (
attribute.id).join(":")}
+ key={variant.attributes
+ .map(attribute => attribute.values[0])
+ .join(":")}
>
{getVariantName(variant, attributes).map(
diff --git a/src/products/components/ProductVariantCreateDialog/createVariants.test.ts b/src/products/components/ProductVariantCreateDialog/createVariants.test.ts
index 86b2230e9..1ecc83b45 100644
--- a/src/products/components/ProductVariantCreateDialog/createVariants.test.ts
+++ b/src/products/components/ProductVariantCreateDialog/createVariants.test.ts
@@ -2,7 +2,7 @@ import {
createVariantFlatMatrixDimension,
createVariants
} from "./createVariants";
-import { thirdStep } from "./fixtures";
+import { attributes, thirdStep } from "./fixtures";
import { ProductVariantCreateFormData } from "./form";
describe("Creates variant matrix", () => {
@@ -35,9 +35,186 @@ describe("Creates variant matrix", () => {
};
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);
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));
+ });
+ });
+ });
});
diff --git a/src/products/components/ProductVariantCreateDialog/createVariants.ts b/src/products/components/ProductVariantCreateDialog/createVariants.ts
index fa8072136..d39bc64e9 100644
--- a/src/products/components/ProductVariantCreateDialog/createVariants.ts
+++ b/src/products/components/ProductVariantCreateDialog/createVariants.ts
@@ -1,32 +1,42 @@
import { ProductVariantCreateInput } from "@saleor/types/globalTypes";
-import { Attribute, ProductVariantCreateFormData } from "./form";
+import {
+ AllOrAttribute,
+ Attribute,
+ ProductVariantCreateFormData
+} from "./form";
interface CreateVariantAttributeValueInput {
attributeId: string;
attributeValueId: string;
}
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(
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;
+ : getAttributeValuePriceOrStock(attributes, data.price);
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,
+ : getAttributeValuePriceOrStock(attributes, data.stock),
10
);
@@ -81,6 +91,12 @@ export function createVariantFlatMatrixDimension(
export function createVariants(
data: ProductVariantCreateFormData
): ProductVariantCreateInput[] {
+ if (
+ (!data.price.all && !data.price.attribute) ||
+ (!data.stock.all && !data.stock.attribute)
+ ) {
+ return [];
+ }
const variants = createVariantFlatMatrixDimension([[]], data.attributes).map(
variant => createVariant(data, variant)
);
diff --git a/src/products/components/ProductVariantCreateDialog/fixtures.ts b/src/products/components/ProductVariantCreateDialog/fixtures.ts
index 1a6180a2e..9601839f0 100644
--- a/src/products/components/ProductVariantCreateDialog/fixtures.ts
+++ b/src/products/components/ProductVariantCreateDialog/fixtures.ts
@@ -1,4 +1,9 @@
-import { initialForm, ProductVariantCreateFormData } from "./form";
+import { createVariants } from "./createVariants";
+import {
+ AllOrAttribute,
+ initialForm,
+ ProductVariantCreateFormData
+} from "./form";
export const attributes = [
{
@@ -44,6 +49,7 @@ export const secondStep: ProductVariantCreateFormData = {
}
]
};
+
export const thirdStep: ProductVariantCreateFormData = {
...secondStep,
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
+ })
+};
diff --git a/src/products/components/ProductVariantCreateDialog/reducer.test.ts b/src/products/components/ProductVariantCreateDialog/reducer.test.ts
index fecca566c..5643c9816 100644
--- a/src/products/components/ProductVariantCreateDialog/reducer.test.ts
+++ b/src/products/components/ProductVariantCreateDialog/reducer.test.ts
@@ -1,6 +1,6 @@
-import { attributes, secondStep, thirdStep } from "./fixtures";
+import { attributes, fourthStep, secondStep, thirdStep } from "./fixtures";
import { initialForm } from "./form";
-import reducer from "./reducer";
+import reducer, { VariantField } from "./reducer";
function execActions(
initialState: TState,
@@ -108,6 +108,7 @@ describe("Reducer is able to", () => {
});
it("select price to each attribute value", () => {
+ const attribute = thirdStep.attributes[0];
const value = 45.99;
const state = execActions(thirdStep, reducer, [
{
@@ -115,18 +116,18 @@ describe("Reducer is able to", () => {
type: "applyPriceToAll"
},
{
- attributeId: attributes[0].id,
+ attributeId: attribute.id,
type: "changeApplyPriceToAttributeId"
},
{
type: "changeAttributeValuePrice",
value: value.toString(),
- valueId: attributes[0].values[0]
+ valueId: attribute.values[0]
},
{
type: "changeAttributeValuePrice",
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", () => {
+ const attribute = thirdStep.attributes[0];
const value = 13;
const state = execActions(thirdStep, reducer, [
{
@@ -146,18 +148,18 @@ describe("Reducer is able to", () => {
type: "applyStockToAll"
},
{
- attributeId: attributes[0].id,
+ attributeId: attribute.id,
type: "changeApplyStockToAttributeId"
},
{
type: "changeAttributeValueStock",
value: value.toString(),
- valueId: attributes[0].values[0]
+ valueId: attribute.values[0]
},
{
type: "changeAttributeValueStock",
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();
});
+
+ 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
+ );
+ });
});
diff --git a/src/products/components/ProductVariantCreateDialog/reducer.ts b/src/products/components/ProductVariantCreateDialog/reducer.ts
index e8954066c..045685921 100644
--- a/src/products/components/ProductVariantCreateDialog/reducer.ts
+++ b/src/products/components/ProductVariantCreateDialog/reducer.ts
@@ -13,14 +13,19 @@ export type ProductVariantCreateReducerActionType =
| "changeApplyStockToAttributeId"
| "changeAttributeValuePrice"
| "changeAttributeValueStock"
+ | "changeVariantData"
| "selectAttribute"
| "selectValue";
+
+export type VariantField = "stock" | "price" | "sku";
export interface ProductVariantCreateReducerAction {
all?: boolean;
attributeId?: string;
+ field?: VariantField;
type: ProductVariantCreateReducerActionType;
value?: string;
valueId?: string;
+ variantIndex?: number;
}
function selectAttribute(
@@ -69,26 +74,36 @@ function applyPriceToAll(
state: ProductVariantCreateFormData,
value: boolean
): ProductVariantCreateFormData {
- return {
+ const data = {
...state,
price: {
...state.price,
all: value
}
};
+
+ return {
+ ...data,
+ variants: createVariants(data)
+ };
}
function applyStockToAll(
state: ProductVariantCreateFormData,
value: boolean
): ProductVariantCreateFormData {
- return {
+ const data = {
...state,
stock: {
...state.stock,
all: value
}
};
+
+ return {
+ ...data,
+ variants: createVariants(data)
+ };
}
function changeAttributeValuePrice(
@@ -113,13 +128,18 @@ function changeAttributeValuePrice(
index
);
- return {
+ const data = {
...state,
price: {
...state.price,
values
}
};
+
+ return {
+ ...data,
+ variants: createVariants(data)
+ };
}
function changeAttributeValueStock(
@@ -144,13 +164,18 @@ function changeAttributeValueStock(
index
);
- return {
+ const data = {
...state,
stock: {
...state.stock,
values
}
};
+
+ return {
+ ...data,
+ variants: createVariants(data)
+ };
}
function changeApplyPriceToAttributeId(
@@ -164,8 +189,7 @@ function changeApplyPriceToAttributeId(
id,
value: ""
}));
-
- return {
+ const data = {
...state,
price: {
...state.price,
@@ -173,38 +197,56 @@ function changeApplyPriceToAttributeId(
values
}
};
+
+ return {
+ ...data,
+ variants: createVariants(data)
+ };
}
function changeApplyStockToAttributeId(
state: ProductVariantCreateFormData,
- attribute: string
+ attributeId: string
): ProductVariantCreateFormData {
- return {
+ const attribute = state.attributes.find(
+ attribute => attribute.id === attributeId
+ );
+ const values = attribute.values.map(id => ({
+ id,
+ value: ""
+ }));
+
+ const data = {
...state,
stock: {
...state.stock,
- attribute,
- values: state.attributes
- .find(stateAttribute => stateAttribute.id === attribute)
- .values.map(attributeValue => ({
- id: attributeValue,
- value: ""
- }))
+ attribute: attributeId,
+ values
}
};
+
+ return {
+ ...data,
+ variants: createVariants(data)
+ };
}
function changeApplyPriceToAllValue(
state: ProductVariantCreateFormData,
value: string
): ProductVariantCreateFormData {
- return {
+ const data = {
...state,
price: {
...state.price,
value
}
};
+
+ return {
+ ...data,
+ variants: createVariants(data)
+ };
}
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(
prevState: ProductVariantCreateFormData,
action: ProductVariantCreateReducerAction
@@ -252,6 +315,13 @@ function reduceProductVariantCreateFormData(
return changeApplyPriceToAllValue(prevState, action.value);
case "changeApplyStockToAllValue":
return changeApplyStockToAllValue(prevState, action.value);
+ case "changeVariantData":
+ return changeVariantData(
+ prevState,
+ action.field,
+ action.value,
+ action.variantIndex
+ );
default:
return prevState;
}