diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreate.stories.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreate.stories.tsx index d855f58ab..f181027ed 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreate.stories.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreate.stories.tsx @@ -4,55 +4,56 @@ import { storiesOf } from "@storybook/react"; import React from "react"; import { attributes } from "@saleor/attributes/fixtures"; -import { isSelected } from "@saleor/utils/lists"; import Decorator from "../../../storybook/Decorator"; +import { createVariants } from "./createVariants"; +import { AllOrAttribute } from "./form"; import ProductVariantCreateContent, { ProductVariantCreateContentProps } from "./ProductVariantCreateContent"; import ProductVariantCreateDialog from "./ProductVariantCreateDialog"; -const selectedAttributes = [1, 2, 4].map(index => attributes[index].id); -const selectedValues = attributes - .filter(attribute => - isSelected(attribute.id, selectedAttributes, (a, b) => a === b) - ) - .map(attribute => attribute.values.map(value => value.id)) - .reduce((acc, curr) => [...acc, ...curr], []) - .filter((_, valueIndex) => valueIndex % 2); +const selectedAttributes = [1, 4, 5].map(index => attributes[index]); + +const price: AllOrAttribute = { + all: false, + attribute: selectedAttributes[1].id, + value: "2.79", + values: selectedAttributes[1].values.map((attribute, attributeIndex) => ({ + 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 = { attributes, currencySymbol: "USD", data: { - attributes: selectedAttributes, - price: { - all: false, - attribute: selectedAttributes[1], - value: "2.79", - values: selectedAttributes.map((_, attributeIndex) => - (attributeIndex + 4).toFixed(2) - ) - }, - 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" - } - ] + attributes: dataAttributes, + price, + stock, + variants: createVariants({ + attributes: dataAttributes, + price, + stock, + variants: [] + }) }, dispatchFormDataAction: () => undefined, step: "attributes" @@ -64,7 +65,7 @@ storiesOf("Views / Products / Create multiple variants", module) style={{ margin: "auto", overflow: "visible", - width: 600 + width: 800 }} > {storyFn()} diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateAttributes.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateAttributes.tsx index 2b176c7b1..5de61f721 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateAttributes.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateAttributes.tsx @@ -43,7 +43,7 @@ const ProductVariantCreateAttributes: React.FC< return null; } const isChecked = !!data.attributes.find( - selectedAttribute => selectedAttribute === attribute.id + selectedAttribute => selectedAttribute.id === attribute.id ); return ( diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx index 87a0ccd83..99bb9a27e 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx @@ -40,7 +40,11 @@ const ProductVariantCreateContent: React.FC< const classes = useStyles(props); 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 ( @@ -51,9 +55,9 @@ const ProductVariantCreateContent: React.FC< + onAttributeClick={attributeId => dispatchFormDataAction({ - id, + attributeId, type: "selectAttribute" }) } @@ -63,10 +67,11 @@ const ProductVariantCreateContent: React.FC< + onValueClick={(attributeId, valueId) => dispatchFormDataAction({ - id, - type: "selectValue" + attributeId, + type: "selectValue", + valueId }) } /> @@ -90,19 +95,23 @@ const ProductVariantCreateContent: React.FC< value }) } - onAttributeSelect={(id, type) => + onAttributeSelect={(attributeId, type) => dispatchFormDataAction({ - id, + attributeId, type: type === "price" ? "changeApplyPriceToAttributeId" : "changeApplyStockToAttributeId" }) } - onValueClick={id => + onAttributeValueChange={(valueId, value, type) => dispatchFormDataAction({ - id, - type: "selectValue" + type: + type === "price" + ? "changeAttributeValuePrice" + : "changeAttributeValueStock", + value, + valueId }) } /> diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx index 4143e63a3..8ba0e5a9d 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx @@ -21,7 +21,7 @@ const useStyles = makeStyles((theme: Theme) => ({ content: { overflowX: "visible", overflowY: "hidden", - width: 600 + width: 800 } })); diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx index 324feb070..b81f4a54b 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx @@ -13,7 +13,6 @@ import Grid from "@saleor/components/Grid"; import Hr from "@saleor/components/Hr"; import SingleSelectField from "@saleor/components/SingleSelectField"; import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; -import { isSelected } from "@saleor/utils/lists"; import { ProductVariantCreateFormData } from "./form"; const useStyles = makeStyles((theme: Theme) => ({ @@ -33,10 +32,14 @@ export type PriceOrStock = "price" | "stock"; export interface ProductVariantCreatePricesProps { attributes: ProductDetails_product_productType_variantAttributes[]; data: ProductVariantCreateFormData; - onValueClick: (id: string) => void; - onAttributeSelect: (id: string, type: PriceOrStock) => void; onApplyPriceOrStockChange: (applyToAll: boolean, 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< @@ -47,31 +50,25 @@ const ProductVariantCreatePrices: React.FC< data, onApplyPriceOrStockChange, onApplyToAllChange, - onAttributeSelect + onAttributeSelect, + onAttributeValueChange } = props; const classes = useStyles(props); const intl = useIntl(); - const selectedAttributes = attributes.filter(attribute => - isSelected(attribute.id, data.attributes, (a, b) => a === b) - ); - const attributeChoices = selectedAttributes.map(attribute => ({ + const attributeChoices = attributes.map(attribute => ({ label: attribute.name, value: attribute.id })); const priceAttributeValues = data.price.all ? null : data.price.attribute - ? selectedAttributes.find( - attribute => attribute.id === data.price.attribute - ).values + ? attributes.find(attribute => attribute.id === data.price.attribute).values : []; const stockAttributeValues = data.stock.all ? null : data.stock.attribute - ? selectedAttributes.find( - attribute => attribute.id === data.stock.attribute - ).values + ? attributes.find(attribute => attribute.id === data.stock.attribute).values : []; return ( @@ -142,27 +139,36 @@ const ProductVariantCreatePrices: React.FC< {priceAttributeValues && - priceAttributeValues.map((attribute, attributeIndex) => ( - <> - - -
- {attribute.name} -
-
- -
-
- - ))} + priceAttributeValues.map( + (attributeValue, attributeValueIndex) => ( + <> + + +
+ {attributeValue.name} +
+
+ + onAttributeValueChange( + attributeValue.id, + event.target.value, + "price" + ) + } + /> +
+
+ + ) + )} )} @@ -233,27 +239,36 @@ const ProductVariantCreatePrices: React.FC< {stockAttributeValues && - stockAttributeValues.map((attribute, attributeIndex) => ( - <> - - -
- {attribute.name} -
-
- -
-
- - ))} + 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 c2953e425..210a6d4fa 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx @@ -32,13 +32,13 @@ const useStyles = makeStyles((theme: Theme) => ({ width: "auto" }, colPrice: { - width: 110 + width: 160 }, colSku: { - width: 110 + width: 210 }, colStock: { - width: 110 + width: 160 }, hr: { marginBottom: theme.spacing.unit, diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx index 84327bfd5..801dc222f 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx @@ -14,7 +14,7 @@ import { ProductVariantCreateFormData } from "./form"; export interface ProductVariantCreateValuesProps { attributes: ProductDetails_product_productType_variantAttributes[]; data: ProductVariantCreateFormData; - onValueClick: (id: string) => void; + onValueClick: (attributeId: string, valueId: string) => void; } const useStyles = makeStyles((theme: Theme) => ({ @@ -47,10 +47,16 @@ const ProductVariantCreateValues: React.FC<
{attribute.values.map(value => ( a === b)} + checked={isSelected( + value.id, + data.attributes.find( + dataAttribute => attribute.id === dataAttribute.id + ).values, + (a, b) => a === b + )} name={`value:${value.id}`} label={value.name} - onChange={() => onValueClick(value.id)} + onChange={() => onValueClick(attribute.id, value.id)} /> ))}
diff --git a/src/products/components/ProductVariantCreateDialog/__snapshots__/reducer.test.ts.snap b/src/products/components/ProductVariantCreateDialog/__snapshots__/reducer.test.ts.snap new file mode 100644 index 000000000..d1b59f93b --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/__snapshots__/reducer.test.ts.snap @@ -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 [], +} +`; diff --git a/src/products/components/ProductVariantCreateDialog/createVariants.test.ts b/src/products/components/ProductVariantCreateDialog/createVariants.test.ts new file mode 100644 index 000000000..86b2230e9 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/createVariants.test.ts @@ -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); + }); + }); +}); diff --git a/src/products/components/ProductVariantCreateDialog/createVariants.ts b/src/products/components/ProductVariantCreateDialog/createVariants.ts new file mode 100644 index 000000000..fa8072136 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/createVariants.ts @@ -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; +} diff --git a/src/products/components/ProductVariantCreateDialog/fixtures.ts b/src/products/components/ProductVariantCreateDialog/fixtures.ts new file mode 100644 index 000000000..1a6180a2e --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/fixtures.ts @@ -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]) + } + ] +}; diff --git a/src/products/components/ProductVariantCreateDialog/form.ts b/src/products/components/ProductVariantCreateDialog/form.ts index 503ffbc8b..157ad2c55 100644 --- a/src/products/components/ProductVariantCreateDialog/form.ts +++ b/src/products/components/ProductVariantCreateDialog/form.ts @@ -1,16 +1,23 @@ import { ProductVariantCreateInput } from "../../../types/globalTypes"; +export interface AttributeValue { + id: string; + value: string; +} export interface AllOrAttribute { all: boolean; attribute: string; value: string; + values: AttributeValue[]; +} +export interface Attribute { + id: string; values: string[]; } export interface ProductVariantCreateFormData { - attributes: string[]; + attributes: Attribute[]; price: AllOrAttribute; stock: AllOrAttribute; - values: string[]; variants: ProductVariantCreateInput[]; } @@ -28,6 +35,5 @@ export const initialForm: ProductVariantCreateFormData = { value: "", values: [] }, - values: [], variants: [] }; diff --git a/src/products/components/ProductVariantCreateDialog/reducer.test.ts b/src/products/components/ProductVariantCreateDialog/reducer.test.ts new file mode 100644 index 000000000..fecca566c --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/reducer.test.ts @@ -0,0 +1,171 @@ +import { attributes, secondStep, thirdStep } from "./fixtures"; +import { initialForm } from "./form"; +import reducer from "./reducer"; + +function execActions( + 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(); + }); +}); diff --git a/src/products/components/ProductVariantCreateDialog/reducer.ts b/src/products/components/ProductVariantCreateDialog/reducer.ts index 25379b91d..e8954066c 100644 --- a/src/products/components/ProductVariantCreateDialog/reducer.ts +++ b/src/products/components/ProductVariantCreateDialog/reducer.ts @@ -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"; export type ProductVariantCreateReducerActionType = @@ -10,22 +11,30 @@ export type ProductVariantCreateReducerActionType = | "changeApplyPriceToAttributeId" | "changeApplyStockToAllValue" | "changeApplyStockToAttributeId" - | "changeAttributePrice" - | "changeAttributeStock" + | "changeAttributeValuePrice" + | "changeAttributeValueStock" | "selectAttribute" | "selectValue"; export interface ProductVariantCreateReducerAction { all?: boolean; - id?: string; + attributeId?: string; type: ProductVariantCreateReducerActionType; value?: string; + valueId?: string; } function selectAttribute( state: ProductVariantCreateFormData, - attribute: string + attributeId: string ): 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 { ...initialForm, @@ -35,14 +44,24 @@ function selectAttribute( function selectValue( state: ProductVariantCreateFormData, - value: string + attributeId: string, + valueId: string ): 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 { ...initialForm, - attributes: state.attributes, - values + attributes: updatedAttributes }; } @@ -72,13 +91,27 @@ function applyStockToAll( }; } -function changeAttributePrice( +function changeAttributeValuePrice( state: ProductVariantCreateFormData, - attribute: string, + attributeValueId: string, price: string ): ProductVariantCreateFormData { - const index = state.price.values.indexOf(attribute); - const values = updateAtIndex(price, state.price.values, index); + const index = state.price.values.findIndex( + 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 { ...state, @@ -89,13 +122,27 @@ function changeAttributePrice( }; } -function changeAttributeStock( +function changeAttributeValueStock( state: ProductVariantCreateFormData, - attribute: string, + attributeValueId: string, stock: string ): ProductVariantCreateFormData { - const index = state.stock.values.indexOf(attribute); - const values = updateAtIndex(stock, state.stock.values, index); + const index = state.stock.values.findIndex( + 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 { ...state, @@ -108,13 +155,22 @@ function changeAttributeStock( function changeApplyPriceToAttributeId( state: ProductVariantCreateFormData, - attribute: string + attributeId: string ): ProductVariantCreateFormData { + const attribute = state.attributes.find( + attribute => attribute.id === attributeId + ); + const values = attribute.values.map(id => ({ + id, + value: "" + })); + return { ...state, price: { ...state.price, - attribute + attribute: attributeId, + values } }; } @@ -127,7 +183,13 @@ function changeApplyStockToAttributeId( ...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, value: string ): ProductVariantCreateFormData { - return { + const data = { ...state, stock: { ...state.stock, value } }; + + return { + ...data, + variants: createVariants(data) + }; } function reduceProductVariantCreateFormData( @@ -164,29 +231,30 @@ function reduceProductVariantCreateFormData( ) { switch (action.type) { case "selectAttribute": - return selectAttribute(prevState, action.id); + return selectAttribute(prevState, action.attributeId); case "selectValue": - return selectValue(prevState, action.id); + return selectValue(prevState, action.attributeId, action.valueId); case "applyPriceToAll": return applyPriceToAll(prevState, action.all); case "applyStockToAll": return applyStockToAll(prevState, action.all); - case "changeAttributePrice": - return changeAttributePrice(prevState, action.id, action.value); - case "changeAttributeStock": - return changeAttributeStock(prevState, action.id, action.value); + case "changeAttributeValuePrice": + return changeAttributeValuePrice(prevState, action.valueId, action.value); + case "changeAttributeValueStock": + return changeAttributeValueStock(prevState, action.valueId, action.value); case "changeApplyPriceToAttributeId": - return changeApplyPriceToAttributeId(prevState, action.id); + return changeApplyPriceToAttributeId(prevState, action.attributeId); case "changeApplyStockToAttributeId": - return changeApplyStockToAttributeId(prevState, action.id); + return changeApplyStockToAttributeId(prevState, action.attributeId); case "changeApplyPriceToAllValue": return changeApplyPriceToAllValue(prevState, action.value); case "changeApplyStockToAllValue": return changeApplyStockToAllValue(prevState, action.value); + default: + return prevState; } - return prevState; } export default reduceProductVariantCreateFormData;