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; }