From 5a40f619e1f35eba5403620bb7a2152da1801718 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 19 Sep 2019 12:13:04 +0200 Subject: [PATCH 01/32] Add variant creator wip --- src/attributes/fixtures.ts | 6 +- .../ProductVariantCreate.stories.tsx | 94 +++++++ .../ProductVariantCreateAttributes.tsx | 78 ++++++ .../ProductVariantCreateContent.tsx | 123 ++++++++ .../ProductVariantCreateDialog.tsx | 132 +++++++++ .../ProductVariantCreatePrices.tsx | 265 ++++++++++++++++++ .../ProductVariantCreateSummary.tsx | 165 +++++++++++ .../ProductVariantCreateTabs.tsx | 107 +++++++ .../ProductVariantCreateValues.tsx | 64 +++++ .../ProductVariantCreateDialog/form.ts | 33 +++ .../ProductVariantCreateDialog/reducer.ts | 192 +++++++++++++ .../ProductVariantCreateDialog/types.ts | 5 + 12 files changed, 1263 insertions(+), 1 deletion(-) create mode 100644 src/products/components/ProductVariantCreateDialog/ProductVariantCreate.stories.tsx create mode 100644 src/products/components/ProductVariantCreateDialog/ProductVariantCreateAttributes.tsx create mode 100644 src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx create mode 100644 src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx create mode 100644 src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx create mode 100644 src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx create mode 100644 src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.tsx create mode 100644 src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx create mode 100644 src/products/components/ProductVariantCreateDialog/form.ts create mode 100644 src/products/components/ProductVariantCreateDialog/reducer.ts create mode 100644 src/products/components/ProductVariantCreateDialog/types.ts diff --git a/src/attributes/fixtures.ts b/src/attributes/fixtures.ts index 4c236da3a..92ab3ef0d 100644 --- a/src/attributes/fixtures.ts +++ b/src/attributes/fixtures.ts @@ -1,3 +1,4 @@ +import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; import { AttributeInputTypeEnum, AttributeValueType @@ -35,7 +36,10 @@ export const attribute: AttributeDetailsFragment = { visibleInStorefront: true }; -export const attributes: AttributeList_attributes_edges_node[] = [ +export const attributes: Array< + AttributeList_attributes_edges_node & + ProductDetails_product_productType_variantAttributes +> = [ { node: { __typename: "Attribute" as "Attribute", diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreate.stories.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreate.stories.tsx new file mode 100644 index 000000000..d855f58ab --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreate.stories.tsx @@ -0,0 +1,94 @@ +import Card from "@material-ui/core/Card"; +import CardContent from "@material-ui/core/CardContent"; +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 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 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" + } + ] + }, + dispatchFormDataAction: () => undefined, + step: "attributes" +}; + +storiesOf("Views / Products / Create multiple variants", module) + .addDecorator(storyFn => ( + + {storyFn()} + + )) + .addDecorator(Decorator) + .add("choose attributes", () => ) + .add("select values", () => ( + + )) + .add("prices and SKU", () => ( + + )) + .add("summary", () => ( + + )); + +storiesOf("Views / Products / Create multiple variants", module) + .addDecorator(Decorator) + .add("interactive", () => ( + undefined} + onSubmit={() => undefined} + /> + )); diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateAttributes.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateAttributes.tsx new file mode 100644 index 000000000..2b176c7b1 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateAttributes.tsx @@ -0,0 +1,78 @@ +import { Theme } from "@material-ui/core/styles"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableRow from "@material-ui/core/TableRow"; +import makeStyles from "@material-ui/styles/makeStyles"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import Checkbox from "@saleor/components/Checkbox"; +import { maybe, renderCollection } from "@saleor/misc"; +import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; +import { ProductVariantCreateFormData } from "./form"; + +export interface ProductVariantCreateAttributesProps { + attributes: ProductDetails_product_productType_variantAttributes[]; + data: ProductVariantCreateFormData; + onAttributeClick: (id: string) => void; +} + +const useStyles = makeStyles((theme: Theme) => ({ + checkboxCell: { + paddingLeft: 0 + }, + wideCell: { + width: "100%" + } +})); + +const ProductVariantCreateAttributes: React.FC< + ProductVariantCreateAttributesProps +> = props => { + const { attributes, data, onAttributeClick } = props; + const classes = useStyles(props); + + return ( + + + {renderCollection( + attributes, + attribute => { + if (!attribute) { + return null; + } + const isChecked = !!data.attributes.find( + selectedAttribute => selectedAttribute === attribute.id + ); + + return ( + attribute.id)}> + + onAttributeClick(attribute.id)} + /> + + + {attribute.name} + + + ); + }, + () => ( + + + + + + ) + )} + +
+ ); +}; + +ProductVariantCreateAttributes.displayName = "ProductVariantCreateAttributes"; +export default ProductVariantCreateAttributes; diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx new file mode 100644 index 000000000..87a0ccd83 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx @@ -0,0 +1,123 @@ +import React from "react"; + +import { makeStyles } from "@material-ui/styles"; +import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; +import { isSelected } from "@saleor/utils/lists"; +import { ProductVariantCreateFormData } from "./form"; +import ProductVariantCreateAttributes from "./ProductVariantCreateAttributes"; +import ProductVariantCreatePrices from "./ProductVariantCreatePrices"; +import ProductVariantCreateSummary from "./ProductVariantCreateSummary"; +import ProductVariantCreateTabs from "./ProductVariantCreateTabs"; +import ProductVariantCreateValues from "./ProductVariantCreateValues"; +import { ProductVariantCreateReducerAction } from "./reducer"; +import { ProductVariantCreateStep } from "./types"; + +const useStyles = makeStyles({ + root: { + maxHeight: 400, + overflowY: "scroll" + } +}); + +export interface ProductVariantCreateContentProps { + attributes: ProductDetails_product_productType_variantAttributes[]; + currencySymbol: string; + data: ProductVariantCreateFormData; + dispatchFormDataAction: React.Dispatch; + step: ProductVariantCreateStep; +} + +const ProductVariantCreateContent: React.FC< + ProductVariantCreateContentProps +> = props => { + const { + attributes, + currencySymbol, + data, + dispatchFormDataAction, + step + } = props; + const classes = useStyles(props); + + const selectedAttributes = attributes.filter(attribute => + isSelected(attribute.id, data.attributes, (a, b) => a === b) + ); + + return ( +
+ +
+ {step === "attributes" && ( + + dispatchFormDataAction({ + id, + type: "selectAttribute" + }) + } + /> + )} + {step === "values" && ( + + dispatchFormDataAction({ + id, + type: "selectValue" + }) + } + /> + )} + {step === "prices" && ( + + dispatchFormDataAction({ + all, + type: type === "price" ? "applyPriceToAll" : "applyStockToAll" + }) + } + onApplyToAllChange={(value, type) => + dispatchFormDataAction({ + type: + type === "price" + ? "changeApplyPriceToAllValue" + : "changeApplyStockToAllValue", + value + }) + } + onAttributeSelect={(id, type) => + dispatchFormDataAction({ + id, + type: + type === "price" + ? "changeApplyPriceToAttributeId" + : "changeApplyStockToAttributeId" + }) + } + onValueClick={id => + dispatchFormDataAction({ + id, + type: "selectValue" + }) + } + /> + )} + {step === "summary" && ( + + )} +
+
+ ); +}; + +ProductVariantCreateContent.displayName = "ProductVariantCreateContent"; +export default ProductVariantCreateContent; diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx new file mode 100644 index 000000000..4143e63a3 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx @@ -0,0 +1,132 @@ +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import { Theme } from "@material-ui/core/styles"; +import { makeStyles } from "@material-ui/styles"; +import React from "react"; +import { FormattedMessage } from "react-intl"; +import { initialForm, ProductVariantCreateFormData } from "./form"; +import ProductVariantCreateContent, { + ProductVariantCreateContentProps +} from "./ProductVariantCreateContent"; +import reduceProductVariantCreateFormData from "./reducer"; +import { ProductVariantCreateStep } from "./types"; + +const useStyles = makeStyles((theme: Theme) => ({ + button: { + marginLeft: theme.spacing.unit * 2 + }, + content: { + overflowX: "visible", + overflowY: "hidden", + width: 600 + } +})); + +export interface ProductVariantCreateDialogProps + extends Omit< + ProductVariantCreateContentProps, + "dispatchFormDataAction" | "step" + > { + open: boolean; + onClose: () => undefined; + onSubmit: (data: ProductVariantCreateFormData) => void; +} + +const ProductVariantCreateDialog: React.FC< + ProductVariantCreateDialogProps +> = props => { + const { open, onClose, ...contentProps } = props; + const classes = useStyles(props); + const [step, setStep] = React.useState( + "attributes" + ); + + function handleNextStep() { + switch (step) { + case "attributes": + setStep("values"); + break; + case "values": + setStep("prices"); + break; + case "prices": + setStep("summary"); + break; + } + } + + function handlePrevStep() { + switch (step) { + case "values": + setStep("attributes"); + break; + case "prices": + setStep("values"); + break; + case "summary": + setStep("prices"); + break; + } + } + + const [data, dispatchFormDataAction] = React.useReducer( + reduceProductVariantCreateFormData, + initialForm + ); + + return ( + + + + + + + + + + {step !== "attributes" && ( + + )} + {step !== "summary" ? ( + + ) : ( + + )} + + + ); +}; + +ProductVariantCreateDialog.displayName = "ProductVariantCreateDialog"; +export default ProductVariantCreateDialog; diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx new file mode 100644 index 000000000..324feb070 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx @@ -0,0 +1,265 @@ +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Radio from "@material-ui/core/Radio"; +import RadioGroup from "@material-ui/core/RadioGroup"; +import { Theme } from "@material-ui/core/styles"; +import TextField from "@material-ui/core/TextField"; +import Typography from "@material-ui/core/Typography"; +import { makeStyles } from "@material-ui/styles"; +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import FormSpacer from "@saleor/components/FormSpacer"; +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) => ({ + hr: { + marginBottom: theme.spacing.unit, + marginTop: theme.spacing.unit / 2 + }, + label: { + alignSelf: "center" + }, + shortInput: { + width: "50%" + } +})); + +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; +} + +const ProductVariantCreatePrices: React.FC< + ProductVariantCreatePricesProps +> = props => { + const { + attributes, + data, + onApplyPriceOrStockChange, + onApplyToAllChange, + onAttributeSelect + } = 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 => ({ + label: attribute.name, + value: attribute.id + })); + const priceAttributeValues = data.price.all + ? null + : data.price.attribute + ? selectedAttributes.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 + : []; + + return ( + <> + + + +
+ + } + label={intl.formatMessage({ + defaultMessage: "Apply single price to all SKUs" + })} + onChange={() => onApplyPriceOrStockChange(true, "price")} + /> + + onApplyToAllChange(event.target.value, "price")} + /> + + } + label={intl.formatMessage({ + defaultMessage: "Apply unique prices by attribute to each SKU" + })} + onChange={() => onApplyPriceOrStockChange(false, "price")} + /> + {!data.price.all && ( + <> + + +
+ + + +
+
+ + onAttributeSelect(event.target.value, "price") + } + /> +
+
+ {priceAttributeValues && + priceAttributeValues.map((attribute, attributeIndex) => ( + <> + + +
+ {attribute.name} +
+
+ +
+
+ + ))} + + )} +
+ + + + +
+ + } + label={intl.formatMessage({ + defaultMessage: "Apply single stock to all SKUs" + })} + onChange={() => onApplyPriceOrStockChange(true, "stock")} + /> + + onApplyToAllChange(event.target.value, "stock")} + /> + + } + label={intl.formatMessage({ + defaultMessage: "Apply unique stock by attribute to each SKU" + })} + onChange={() => onApplyPriceOrStockChange(false, "stock")} + /> + {!data.stock.all && ( + <> + + +
+ + + +
+
+ + onAttributeSelect(event.target.value, "stock") + } + /> +
+
+ {stockAttributeValues && + stockAttributeValues.map((attribute, attributeIndex) => ( + <> + + +
+ {attribute.name} +
+
+ +
+
+ + ))} + + )} +
+ + ); +}; + +ProductVariantCreatePrices.displayName = "ProductVariantCreatePrices"; +export default ProductVariantCreatePrices; diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx new file mode 100644 index 000000000..c2953e425 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx @@ -0,0 +1,165 @@ +import { Theme } from "@material-ui/core/styles"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import TextField from "@material-ui/core/TextField"; +import Typography from "@material-ui/core/Typography"; +import { makeStyles } from "@material-ui/styles"; +import classNames from "classnames"; +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import Hr from "@saleor/components/Hr"; +import { ProductVariantCreateInput } from "@saleor/types/globalTypes"; +import { ProductDetails_product_productType_variantAttributes } from "../../types/ProductDetails"; +import { ProductVariantCreateFormData } from "./form"; + +export interface ProductVariantCreateSummaryProps { + attributes: ProductDetails_product_productType_variantAttributes[]; + currencySymbol: string; + data: ProductVariantCreateFormData; +} + +const useStyles = makeStyles((theme: Theme) => ({ + col: { + paddingLeft: theme.spacing.unit, + paddingRight: theme.spacing.unit + }, + colName: { + paddingLeft: "0 !important", + width: "auto" + }, + colPrice: { + width: 110 + }, + colSku: { + width: 110 + }, + colStock: { + width: 110 + }, + hr: { + marginBottom: theme.spacing.unit, + marginTop: theme.spacing.unit / 2 + }, + input: { + "& input": { + padding: "16px 12px 17px" + }, + marginTop: theme.spacing.unit / 2, + width: 104 + } +})); + +function getVariantName( + variant: ProductVariantCreateInput, + attributes: ProductDetails_product_productType_variantAttributes[] +): string[] { + return attributes.reduce( + (acc, attribute) => [ + ...acc, + attribute.values.find( + value => + value.id === + variant.attributes.find( + variantAttribute => variantAttribute.id === attribute.id + ).values[0] + ).name + ], + [] + ); +} + +const ProductVariantCreateSummary: React.FC< + ProductVariantCreateSummaryProps +> = props => { + const { attributes, currencySymbol, data } = props; + const classes = useStyles(props); + + return ( + <> + + + +
+ + + + + + + + + + + + + + + + + + + {data.variants.map(variant => ( + attribute.id).join(":")} + > + + {getVariantName(variant, attributes).join(" ")} + + + + + + + + + + + + ))} + +
+ + ); +}; + +ProductVariantCreateSummary.displayName = "ProductVariantCreateSummary"; +export default ProductVariantCreateSummary; diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.tsx new file mode 100644 index 000000000..11a57da01 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.tsx @@ -0,0 +1,107 @@ +import { Theme } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import { makeStyles } from "@material-ui/styles"; +import classNames from "classnames"; +import React from "react"; +import { IntlShape, useIntl } from "react-intl"; + +import { ProductVariantCreateStep } from "./types"; + +interface Step { + label: string; + value: ProductVariantCreateStep; +} +function getSteps(intl: IntlShape): Step[] { + return [ + { + label: intl.formatMessage({ + defaultMessage: "Choose Attributes", + description: "variant creation step" + }), + value: "attributes" + }, + { + label: intl.formatMessage({ + defaultMessage: "Select Values", + description: "attribute values, variant creation step" + }), + value: "values" + }, + { + label: intl.formatMessage({ + defaultMessage: "Prices and SKU", + description: "variant creation step" + }), + value: "prices" + }, + { + label: intl.formatMessage({ + defaultMessage: "Summary", + description: "variant creation step" + }), + value: "summary" + } + ]; +} + +const useStyles = makeStyles( + (theme: Theme) => ({ + label: { + fontSize: 14, + textAlign: "center" + }, + root: { + borderBottom: `1px solid ${theme.palette.divider}`, + display: "flex", + justifyContent: "space-between", + marginBottom: theme.spacing.unit * 3 + }, + tab: { + flex: 1, + paddingBottom: theme.spacing.unit + }, + tabActive: { + fontWeight: 600 + }, + tabUnderline: { + borderBottom: `3px solid ${theme.palette.primary.main}` + } + }), + { + name: "ProductVariantCreateTabs" + } +); + +export interface ProductVariantCreateTabsProps { + step: ProductVariantCreateStep; +} + +const ProductVariantCreateTabs: React.FC< + ProductVariantCreateTabsProps +> = props => { + const { step: currentStep } = props; + const classes = useStyles(props); + const intl = useIntl(); + const steps = getSteps(intl); + + return ( +
+ {steps.map((step, stepIndex) => ( +
step.value === currentStep) >= stepIndex + })} + > + + {step.label} + +
+ ))} +
+ ); +}; + +ProductVariantCreateTabs.displayName = "ProductVariantCreateTabs"; +export default ProductVariantCreateTabs; diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx new file mode 100644 index 000000000..84327bfd5 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx @@ -0,0 +1,64 @@ +import { Theme } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import makeStyles from "@material-ui/styles/makeStyles"; +import React from "react"; + +import ControlledCheckbox from "@saleor/components/ControlledCheckbox"; +import Hr from "@saleor/components/Hr"; +import Skeleton from "@saleor/components/Skeleton"; +import { maybe } from "@saleor/misc"; +import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; +import { isSelected } from "@saleor/utils/lists"; +import { ProductVariantCreateFormData } from "./form"; + +export interface ProductVariantCreateValuesProps { + attributes: ProductDetails_product_productType_variantAttributes[]; + data: ProductVariantCreateFormData; + onValueClick: (id: string) => void; +} + +const useStyles = makeStyles((theme: Theme) => ({ + hr: { + marginBottom: theme.spacing.unit, + marginTop: theme.spacing.unit / 2 + }, + valueContainer: { + display: "grid", + gridColumnGap: theme.spacing.unit * 3 + "px", + gridTemplateColumns: "repeat(3, 1fr)", + marginBottom: theme.spacing.unit * 3 + } +})); + +const ProductVariantCreateValues: React.FC< + ProductVariantCreateValuesProps +> = props => { + const { attributes, data, onValueClick } = props; + const classes = useStyles(props); + + return ( + <> + {attributes.map(attribute => ( + <> + + {maybe(() => attribute.name, )} + +
+
+ {attribute.values.map(value => ( + a === b)} + name={`value:${value.id}`} + label={value.name} + onChange={() => onValueClick(value.id)} + /> + ))} +
+ + ))} + + ); +}; + +ProductVariantCreateValues.displayName = "ProductVariantCreateValues"; +export default ProductVariantCreateValues; diff --git a/src/products/components/ProductVariantCreateDialog/form.ts b/src/products/components/ProductVariantCreateDialog/form.ts new file mode 100644 index 000000000..503ffbc8b --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/form.ts @@ -0,0 +1,33 @@ +import { ProductVariantCreateInput } from "../../../types/globalTypes"; + +export interface AllOrAttribute { + all: boolean; + attribute: string; + value: string; + values: string[]; +} +export interface ProductVariantCreateFormData { + attributes: string[]; + price: AllOrAttribute; + stock: AllOrAttribute; + values: string[]; + variants: ProductVariantCreateInput[]; +} + +export const initialForm: ProductVariantCreateFormData = { + attributes: [], + price: { + all: true, + attribute: undefined, + value: "", + values: [] + }, + stock: { + all: true, + attribute: undefined, + value: "", + values: [] + }, + values: [], + variants: [] +}; diff --git a/src/products/components/ProductVariantCreateDialog/reducer.ts b/src/products/components/ProductVariantCreateDialog/reducer.ts new file mode 100644 index 000000000..25379b91d --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/reducer.ts @@ -0,0 +1,192 @@ +import { toggle, updateAtIndex } from "@saleor/utils/lists"; +import { initialForm, ProductVariantCreateFormData } from "./form"; + +export type ProductVariantCreateReducerActionType = + | "applyPriceToAll" + | "applyPriceToAttribute" + | "applyStockToAll" + | "applyStockToAttribute" + | "changeApplyPriceToAllValue" + | "changeApplyPriceToAttributeId" + | "changeApplyStockToAllValue" + | "changeApplyStockToAttributeId" + | "changeAttributePrice" + | "changeAttributeStock" + | "selectAttribute" + | "selectValue"; +export interface ProductVariantCreateReducerAction { + all?: boolean; + id?: string; + type: ProductVariantCreateReducerActionType; + value?: string; +} + +function selectAttribute( + state: ProductVariantCreateFormData, + attribute: string +): ProductVariantCreateFormData { + const attributes = toggle(attribute, state.attributes, (a, b) => a === b); + + return { + ...initialForm, + attributes + }; +} + +function selectValue( + state: ProductVariantCreateFormData, + value: string +): ProductVariantCreateFormData { + const values = toggle(value, state.values, (a, b) => a === b); + + return { + ...initialForm, + attributes: state.attributes, + values + }; +} + +function applyPriceToAll( + state: ProductVariantCreateFormData, + value: boolean +): ProductVariantCreateFormData { + return { + ...state, + price: { + ...state.price, + all: value + } + }; +} + +function applyStockToAll( + state: ProductVariantCreateFormData, + value: boolean +): ProductVariantCreateFormData { + return { + ...state, + stock: { + ...state.stock, + all: value + } + }; +} + +function changeAttributePrice( + state: ProductVariantCreateFormData, + attribute: string, + price: string +): ProductVariantCreateFormData { + const index = state.price.values.indexOf(attribute); + const values = updateAtIndex(price, state.price.values, index); + + return { + ...state, + price: { + ...state.price, + values + } + }; +} + +function changeAttributeStock( + state: ProductVariantCreateFormData, + attribute: string, + stock: string +): ProductVariantCreateFormData { + const index = state.stock.values.indexOf(attribute); + const values = updateAtIndex(stock, state.stock.values, index); + + return { + ...state, + stock: { + ...state.stock, + values + } + }; +} + +function changeApplyPriceToAttributeId( + state: ProductVariantCreateFormData, + attribute: string +): ProductVariantCreateFormData { + return { + ...state, + price: { + ...state.price, + attribute + } + }; +} + +function changeApplyStockToAttributeId( + state: ProductVariantCreateFormData, + attribute: string +): ProductVariantCreateFormData { + return { + ...state, + stock: { + ...state.stock, + attribute + } + }; +} + +function changeApplyPriceToAllValue( + state: ProductVariantCreateFormData, + value: string +): ProductVariantCreateFormData { + return { + ...state, + price: { + ...state.price, + value + } + }; +} + +function changeApplyStockToAllValue( + state: ProductVariantCreateFormData, + value: string +): ProductVariantCreateFormData { + return { + ...state, + stock: { + ...state.stock, + value + } + }; +} + +function reduceProductVariantCreateFormData( + prevState: ProductVariantCreateFormData, + action: ProductVariantCreateReducerAction +) { + switch (action.type) { + case "selectAttribute": + return selectAttribute(prevState, action.id); + + case "selectValue": + return selectValue(prevState, action.id); + + 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 "changeApplyPriceToAttributeId": + return changeApplyPriceToAttributeId(prevState, action.id); + case "changeApplyStockToAttributeId": + return changeApplyStockToAttributeId(prevState, action.id); + case "changeApplyPriceToAllValue": + return changeApplyPriceToAllValue(prevState, action.value); + case "changeApplyStockToAllValue": + return changeApplyStockToAllValue(prevState, action.value); + } + return prevState; +} + +export default reduceProductVariantCreateFormData; diff --git a/src/products/components/ProductVariantCreateDialog/types.ts b/src/products/components/ProductVariantCreateDialog/types.ts new file mode 100644 index 000000000..944b94e89 --- /dev/null +++ b/src/products/components/ProductVariantCreateDialog/types.ts @@ -0,0 +1,5 @@ +export type ProductVariantCreateStep = + | "attributes" + | "values" + | "prices" + | "summary"; From b87b0dcb6c5cde52cce229832ee90ff31196b6c6 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 19 Sep 2019 12:13:21 +0200 Subject: [PATCH 02/32] Fix invisible adornments --- src/components/Timeline/Timeline.tsx | 3 +++ src/theme.ts | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Timeline/Timeline.tsx b/src/components/Timeline/Timeline.tsx index 0bb77b6b5..1c449c8e8 100644 --- a/src/components/Timeline/Timeline.tsx +++ b/src/components/Timeline/Timeline.tsx @@ -30,6 +30,9 @@ const styles = (theme: Theme) => "& > div": { padding: "0 14px" }, + "& fieldset": { + background: theme.palette.background.paper + }, "& textarea": { "&::placeholder": { opacity: [[1], "!important"] as any diff --git a/src/theme.ts b/src/theme.ts index c8d42b7a8..e523b0d45 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -281,8 +281,7 @@ export default (colors: IThemeColors): Theme => "& fieldset": { "&&:not($error)": { borderColor: colors.input.border - }, - background: colors.background.paper + } }, "& legend": { display: "none" From 742523bac721e9c67404ee3e6f6bf22e1006f1d7 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 19 Sep 2019 12:13:48 +0200 Subject: [PATCH 03/32] Fix variant create query --- .../ProductVariantCreateDialog/index.ts | 0 src/products/mutations.ts | 22 ++----------------- src/products/queries.ts | 10 +++++++++ src/products/types/ProductDetails.ts | 14 ++++++++++++ src/products/types/VariantCreate.ts | 10 ++------- src/types/globalTypes.ts | 11 ++++++++++ 6 files changed, 39 insertions(+), 28 deletions(-) create mode 100644 src/products/components/ProductVariantCreateDialog/index.ts diff --git a/src/products/components/ProductVariantCreateDialog/index.ts b/src/products/components/ProductVariantCreateDialog/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/products/mutations.ts b/src/products/mutations.ts index ef7024d69..563c09f84 100644 --- a/src/products/mutations.ts +++ b/src/products/mutations.ts @@ -319,26 +319,8 @@ export const TypedVariantUpdateMutation = TypedMutation< export const variantCreateMutation = gql` ${fragmentVariant} - mutation VariantCreate( - $attributes: [AttributeValueInput]! - $costPrice: Decimal - $priceOverride: Decimal - $product: ID! - $sku: String - $quantity: Int - $trackInventory: Boolean! - ) { - productVariantCreate( - input: { - attributes: $attributes - costPrice: $costPrice - priceOverride: $priceOverride - product: $product - sku: $sku - quantity: $quantity - trackInventory: $trackInventory - } - ) { + mutation VariantCreate($input: ProductVariantCreateInput!) { + productVariantCreate(input: $input) { errors { field message diff --git a/src/products/queries.ts b/src/products/queries.ts index fa60db7cb..efbdb9489 100644 --- a/src/products/queries.ts +++ b/src/products/queries.ts @@ -260,6 +260,16 @@ const productDetailsQuery = gql` query ProductDetails($id: ID!) { product(id: $id) { ...Product + productType { + variantAttributes { + id + name + values { + id + name + } + } + } } } `; diff --git a/src/products/types/ProductDetails.ts b/src/products/types/ProductDetails.ts index 5094afd59..803e66d8d 100644 --- a/src/products/types/ProductDetails.ts +++ b/src/products/types/ProductDetails.ts @@ -139,11 +139,25 @@ export interface ProductDetails_product_variants { stockQuantity: number; } +export interface ProductDetails_product_productType_variantAttributes_values { + __typename: "AttributeValue"; + id: string; + name: string | null; +} + +export interface ProductDetails_product_productType_variantAttributes { + __typename: "Attribute"; + id: string; + name: string | null; + values: (ProductDetails_product_productType_variantAttributes_values | null)[] | null; +} + export interface ProductDetails_product_productType { __typename: "ProductType"; id: string; name: string; hasVariants: boolean; + variantAttributes: (ProductDetails_product_productType_variantAttributes | null)[] | null; } export interface ProductDetails_product { diff --git a/src/products/types/VariantCreate.ts b/src/products/types/VariantCreate.ts index 4dcfdd5d1..b1d2783b1 100644 --- a/src/products/types/VariantCreate.ts +++ b/src/products/types/VariantCreate.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { AttributeValueInput } from "./../../types/globalTypes"; +import { ProductVariantCreateInput } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: VariantCreate @@ -122,11 +122,5 @@ export interface VariantCreate { } export interface VariantCreateVariables { - attributes: (AttributeValueInput | null)[]; - costPrice?: any | null; - priceOverride?: any | null; - product: string; - sku?: string | null; - quantity?: number | null; - trackInventory: boolean; + input: ProductVariantCreateInput; } diff --git a/src/types/globalTypes.ts b/src/types/globalTypes.ts index 1e8cfaf3a..5dff42bb4 100644 --- a/src/types/globalTypes.ts +++ b/src/types/globalTypes.ts @@ -614,6 +614,17 @@ export interface ProductTypeInput { taxCode?: string | null; } +export interface ProductVariantCreateInput { + attributes: (AttributeValueInput | null)[]; + costPrice?: any | null; + priceOverride?: any | null; + sku?: string | null; + quantity?: number | null; + trackInventory?: boolean | null; + weight?: any | null; + product: string; +} + export interface ProductVariantInput { attributes?: (AttributeValueInput | null)[] | null; costPrice?: any | null; From 2879a80d0d4deb52b4d4c1080b8121b1efe859fd Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Mon, 23 Sep 2019 17:19:41 +0200 Subject: [PATCH 04/32] Add variant matrix generation --- .../ProductVariantCreate.stories.tsx | 79 +-- .../ProductVariantCreateAttributes.tsx | 2 +- .../ProductVariantCreateContent.tsx | 31 +- .../ProductVariantCreateDialog.tsx | 2 +- .../ProductVariantCreatePrices.tsx | 127 ++--- .../ProductVariantCreateSummary.tsx | 6 +- .../ProductVariantCreateValues.tsx | 12 +- .../__snapshots__/reducer.test.ts.snap | 457 ++++++++++++++++++ .../createVariants.test.ts | 43 ++ .../createVariants.ts | 89 ++++ .../ProductVariantCreateDialog/fixtures.ts | 63 +++ .../ProductVariantCreateDialog/form.ts | 12 +- .../reducer.test.ts | 171 +++++++ .../ProductVariantCreateDialog/reducer.ts | 130 +++-- 14 files changed, 1076 insertions(+), 148 deletions(-) create mode 100644 src/products/components/ProductVariantCreateDialog/__snapshots__/reducer.test.ts.snap create mode 100644 src/products/components/ProductVariantCreateDialog/createVariants.test.ts create mode 100644 src/products/components/ProductVariantCreateDialog/createVariants.ts create mode 100644 src/products/components/ProductVariantCreateDialog/fixtures.ts create mode 100644 src/products/components/ProductVariantCreateDialog/reducer.test.ts 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; From ddb2bc1bd445927fa00a1cb55537bca77a1295a9 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Mon, 23 Sep 2019 17:21:26 +0200 Subject: [PATCH 05/32] Fill cell with input --- .../ProductVariantCreateDialog/ProductVariantCreateSummary.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx index 210a6d4fa..5b4deb24d 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx @@ -48,8 +48,7 @@ const useStyles = makeStyles((theme: Theme) => ({ "& input": { padding: "16px 12px 17px" }, - marginTop: theme.spacing.unit / 2, - width: 104 + marginTop: theme.spacing.unit / 2 } })); From 038d6e5580dff38834fd05c4ee7fbb1202f1ab5e Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Mon, 23 Sep 2019 17:27:01 +0200 Subject: [PATCH 06/32] Add value coloring --- .../ProductVariantCreateSummary.tsx | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx index 5b4deb24d..746ceea89 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx @@ -1,3 +1,8 @@ +import blue from "@material-ui/core/colors/blue"; +import cyan from "@material-ui/core/colors/cyan"; +import green from "@material-ui/core/colors/green"; +import purple from "@material-ui/core/colors/purple"; +import yellow from "@material-ui/core/colors/yellow"; import { Theme } from "@material-ui/core/styles"; import Table from "@material-ui/core/Table"; import TableBody from "@material-ui/core/TableBody"; @@ -9,7 +14,7 @@ import Typography from "@material-ui/core/Typography"; import { makeStyles } from "@material-ui/styles"; import classNames from "classnames"; import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; +import { FormattedMessage } from "react-intl"; import Hr from "@saleor/components/Hr"; import { ProductVariantCreateInput } from "@saleor/types/globalTypes"; @@ -22,7 +27,12 @@ export interface ProductVariantCreateSummaryProps { data: ProductVariantCreateFormData; } +const colors = [blue, cyan, green, purple, yellow].map(color => color[500]); + const useStyles = makeStyles((theme: Theme) => ({ + attributeValue: { + marginRight: theme.spacing.unit + }, col: { paddingLeft: theme.spacing.unit, paddingRight: theme.spacing.unit @@ -118,7 +128,18 @@ const ProductVariantCreateSummary: React.FC< key={variant.attributes.map(attribute => attribute.id).join(":")} > - {getVariantName(variant, attributes).join(" ")} + {getVariantName(variant, attributes).map( + (value, valueIndex) => ( + + {value} + + ) + )} Date: Mon, 23 Sep 2019 17:32:00 +0200 Subject: [PATCH 07/32] Visual fixes --- .../ProductVariantCreateSummary.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx index 746ceea89..4d695d5c6 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx @@ -42,13 +42,13 @@ const useStyles = makeStyles((theme: Theme) => ({ width: "auto" }, colPrice: { - width: 160 + width: 200 }, colSku: { width: 210 }, colStock: { - width: 160 + width: 120 }, hr: { marginBottom: theme.spacing.unit, From fcfdd432b4df479acb50b3fb64983329b163975b Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 24 Sep 2019 14:27:11 +0200 Subject: [PATCH 08/32] Make it work --- .../ProductVariantCreatePrices.tsx | 232 +++++++++--------- .../ProductVariantCreateSummary.tsx | 4 +- .../createVariants.test.ts | 179 +++++++++++++- .../createVariants.ts | 42 +++- .../ProductVariantCreateDialog/fixtures.ts | 49 +++- .../reducer.test.ts | 38 ++- .../ProductVariantCreateDialog/reducer.ts | 102 ++++++-- 7 files changed, 488 insertions(+), 158 deletions(-) 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; } From 41cc4b0cf8097fdb4306ff99511fda3111c33ca3 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 24 Sep 2019 14:38:52 +0200 Subject: [PATCH 09/32] Darken colors a bit --- .../ProductVariantCreateDialog/ProductVariantCreateSummary.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx index 62ef027e2..4575178ac 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx @@ -27,7 +27,7 @@ export interface ProductVariantCreateSummaryProps { data: ProductVariantCreateFormData; } -const colors = [blue, cyan, green, purple, yellow].map(color => color[500]); +const colors = [blue, cyan, green, purple, yellow].map(color => color[800]); const useStyles = makeStyles((theme: Theme) => ({ attributeValue: { From cc1a42c5efc9a48b066fd2b11a07acb9eb277041 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 24 Sep 2019 14:44:56 +0200 Subject: [PATCH 10/32] Update snapshots --- .../__snapshots__/reducer.test.ts.snap | 609 +++++++++++++++++- 1 file changed, 606 insertions(+), 3 deletions(-) diff --git a/src/products/components/ProductVariantCreateDialog/__snapshots__/reducer.test.ts.snap b/src/products/components/ProductVariantCreateDialog/__snapshots__/reducer.test.ts.snap index d1b59f93b..e86da0c57 100644 --- a/src/products/components/ProductVariantCreateDialog/__snapshots__/reducer.test.ts.snap +++ b/src/products/components/ProductVariantCreateDialog/__snapshots__/reducer.test.ts.snap @@ -110,7 +110,208 @@ Object { "value": "", "values": Array [], }, - "variants": 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": "45.99", + "product": "", + "quantity": NaN, + }, + 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": "45.99", + "product": "", + "quantity": NaN, + }, + 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": "45.99", + "product": "", + "quantity": NaN, + }, + 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": "45.99", + "product": "", + "quantity": NaN, + }, + 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": "45.99", + "product": "", + "quantity": NaN, + }, + 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": "45.99", + "product": "", + "quantity": NaN, + }, + 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": "45.99", + "product": "", + "quantity": NaN, + }, + 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": "45.99", + "product": "", + "quantity": NaN, + }, + ], } `; @@ -160,7 +361,208 @@ Object { "value": "", "values": Array [], }, - "variants": 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": "45.99", + "product": "", + "quantity": NaN, + }, + 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": "45.99", + "product": "", + "quantity": NaN, + }, + 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": "45.99", + "product": "", + "quantity": NaN, + }, + 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": "45.99", + "product": "", + "quantity": NaN, + }, + 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": "51.99", + "product": "", + "quantity": NaN, + }, + 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": "51.99", + "product": "", + "quantity": NaN, + }, + 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": "51.99", + "product": "", + "quantity": NaN, + }, + 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": "51.99", + "product": "", + "quantity": NaN, + }, + ], } `; @@ -452,6 +854,207 @@ Object { }, ], }, - "variants": 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": 13, + }, + 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": 13, + }, + 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": 13, + }, + 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": 13, + }, + 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": 19, + }, + 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": 19, + }, + 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": 19, + }, + 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": 19, + }, + ], } `; From 52f458e7397c7d1747fda351f995a840e9c68bb0 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 24 Sep 2019 15:41:55 +0200 Subject: [PATCH 11/32] Add ability to modify sku --- .../ProductVariantCreateContent.tsx | 8 +++ .../ProductVariantCreateDialog.tsx | 53 ++++++++++++++++++- .../ProductVariantCreateSummary.tsx | 28 +++++++++- .../createVariants.ts | 3 +- 4 files changed, 88 insertions(+), 4 deletions(-) diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx index 99bb9a27e..9f66a7220 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx @@ -121,6 +121,14 @@ const ProductVariantCreateContent: React.FC< attributes={selectedAttributes} currencySymbol={currencySymbol} data={data} + onVariantDataChange={(variantIndex, field, value) => + dispatchFormDataAction({ + field, + type: "changeVariantData", + value, + variantIndex + }) + } /> )} diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx index 8ba0e5a9d..a83903d54 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx @@ -25,6 +25,51 @@ const useStyles = makeStyles((theme: Theme) => ({ } })); +function canHitNext( + step: ProductVariantCreateStep, + data: ProductVariantCreateFormData +): boolean { + switch (step) { + case "attributes": + return data.attributes.length > 0; + case "values": + return data.attributes.every(attribute => attribute.values.length > 0); + case "prices": + if (data.price.all) { + if (data.price.value === "") { + return false; + } + } else { + if ( + data.price.attribute === "" || + data.price.values.some(attributeValue => attributeValue.value === "") + ) { + return false; + } + } + + if (data.stock.all) { + if (data.stock.value === "") { + return false; + } + } else { + if ( + data.stock.attribute === "" || + data.stock.values.some(attributeValue => attributeValue.value === "") + ) { + return false; + } + } + + return true; + case "summary": + return data.variants.every(variant => variant.sku !== ""); + + default: + return false; + } +} + export interface ProductVariantCreateDialogProps extends Omit< ProductVariantCreateContentProps, @@ -110,13 +155,19 @@ const ProductVariantCreateDialog: React.FC< ) : ( - + + 100g + + + + + + +
+ Coffee Genre +
+
+
+ + +
+
+ Collar +
+
+
+ + + +
+ + + + + +`; + +exports[`Storyshots Views / Products / Create multiple variants summary 1`] = ` +
+
+
+
+
+
+ + Choose Attributes + +
+
+ + Select Values + +
+
+ + Prices and SKU + +
+
+ + Summary + +
+
+
+
+ You will create variants below +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Variant + + Inventory + + Price + + SKU +
+ + 100g + + + Arabica + + + Round + + +
+
+ + +
+
+
+
+
+ + + USD +
+
+
+
+
+ + +
+
+
+ + 100g + + + Arabica + + + Polo + + +
+
+ + +
+
+
+
+
+ + + USD +
+
+
+
+
+ + +
+
+
+ + 500g + + + Arabica + + + Round + + +
+
+ + +
+
+
+
+
+ + + USD +
+
+
+
+
+ + +
+
+
+ + 500g + + + Arabica + + + Polo + + +
+
+ + +
+
+
+
+
+ + + USD +
+
+
+
+
+ + +
+
+
+
+
+
+
+
+`; + exports[`Storyshots Views / Products / Create product When loading 1`] = `
Date: Wed, 2 Oct 2019 15:34:34 +0200 Subject: [PATCH 13/32] Add error handling --- .../ProductUpdatePage/ProductUpdatePage.tsx | 3 + .../ProductVariantCreate.stories.tsx | 38 +- .../ProductVariantCreateAttributes.tsx | 5 +- .../ProductVariantCreateContent.tsx | 4 + .../ProductVariantCreateDialog.tsx | 11 +- .../ProductVariantCreatePrices.tsx | 20 +- .../ProductVariantCreateSummary.tsx | 267 ++- .../ProductVariantCreateValues.tsx | 6 +- .../__snapshots__/reducer.test.ts.snap | 40 +- .../createVariants.test.ts | 8 +- .../createVariants.ts | 17 +- .../ProductVariantCreateDialog/fixtures.ts | 8 +- .../ProductVariantCreateDialog/form.ts | 6 +- .../ProductVariantCreateDialog/reducer.ts | 24 +- .../ProductVariants/ProductVariants.tsx | 18 +- .../containers/ProductUpdateOperations.tsx | 70 +- src/products/mutations.ts | 28 + src/products/queries.ts | 1 + src/products/types/ProductDetails.ts | 1 + .../types/ProductVariantBulkCreate.ts | 38 + src/products/urls.ts | 2 +- .../views/ProductUpdate/ProductUpdate.tsx | 52 + src/products/views/ProductVariantCreate.tsx | 26 +- .../__snapshots__/Stories.test.ts.snap | 1929 +++++++++++------ .../stories/products/ProductUpdatePage.tsx | 1 + src/types/globalTypes.ts | 24 + 26 files changed, 1701 insertions(+), 946 deletions(-) create mode 100644 src/products/types/ProductVariantBulkCreate.ts diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx index ac57ec9fc..f341f5875 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx @@ -60,6 +60,7 @@ export interface ProductUpdatePageProps extends ListActions { saveButtonBarState: ConfirmButtonTransitionState; fetchCategories: (query: string) => void; fetchCollections: (query: string) => void; + onVariantsAdd: () => void; onVariantShow: (id: string) => () => void; onImageDelete: (id: string) => () => void; onBack?(); @@ -100,6 +101,7 @@ export const ProductUpdatePage: React.FC = ({ onSeoClick, onSubmit, onVariantAdd, + onVariantsAdd, onVariantShow, isChecked, selected, @@ -236,6 +238,7 @@ export const ProductUpdatePage: React.FC = ({ fallbackPrice={product ? product.basePrice : undefined} onRowClick={onVariantShow} onVariantAdd={onVariantAdd} + onVariantsAdd={onVariantsAdd} toolbar={toolbar} isChecked={isChecked} selected={selected} diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreate.stories.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreate.stories.tsx index f181027ed..a0738af4c 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreate.stories.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreate.stories.tsx @@ -4,6 +4,8 @@ import { storiesOf } from "@storybook/react"; import React from "react"; import { attributes } from "@saleor/attributes/fixtures"; +import { ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors } from "@saleor/products/types/ProductVariantBulkCreate"; +import { ProductErrorCode } from "@saleor/types/globalTypes"; import Decorator from "../../../storybook/Decorator"; import { createVariants } from "./createVariants"; import { AllOrAttribute } from "./form"; @@ -19,7 +21,7 @@ const price: AllOrAttribute = { attribute: selectedAttributes[1].id, value: "2.79", values: selectedAttributes[1].values.map((attribute, attributeIndex) => ({ - id: attribute.id, + slug: attribute.slug, value: (attributeIndex + 4).toFixed(2) })) }; @@ -29,7 +31,7 @@ const stock: AllOrAttribute = { attribute: selectedAttributes[1].id, value: "8", values: selectedAttributes[1].values.map((attribute, attributeIndex) => ({ - id: attribute.id, + slug: attribute.slug, value: (selectedAttributes.length * 10 - attributeIndex).toString() })) }; @@ -37,10 +39,20 @@ const stock: AllOrAttribute = { const dataAttributes = selectedAttributes.map(attribute => ({ id: attribute.id, values: attribute.values - .map(value => value.id) + .map(value => value.slug) .filter((_, valueIndex) => valueIndex % 2 !== 1) })); +const errors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[] = [ + { + __typename: "BulkProductError", + code: ProductErrorCode.UNIQUE, + field: "sku", + index: 3, + message: "Duplicated SKU." + } +]; + const props: ProductVariantCreateContentProps = { attributes, currencySymbol: "USD", @@ -56,6 +68,7 @@ const props: ProductVariantCreateContentProps = { }) }, dispatchFormDataAction: () => undefined, + errors: [], step: "attributes" }; @@ -78,9 +91,26 @@ storiesOf("Views / Products / Create multiple variants", module) )) .add("prices and SKU", () => ( + )); + +storiesOf("Views / Products / Create multiple variants / summary", module) + .addDecorator(storyFn => ( + + {storyFn()} + )) - .add("summary", () => ( + .addDecorator(Decorator) + .add("default", () => ( + )) + .add("errors", () => ( + )); storiesOf("Views / Products / Create multiple variants", module) diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateAttributes.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateAttributes.tsx index 5de61f721..48b658d9f 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateAttributes.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateAttributes.tsx @@ -1,4 +1,3 @@ -import { Theme } from "@material-ui/core/styles"; import Table from "@material-ui/core/Table"; import TableBody from "@material-ui/core/TableBody"; import TableCell from "@material-ui/core/TableCell"; @@ -18,14 +17,14 @@ export interface ProductVariantCreateAttributesProps { onAttributeClick: (id: string) => void; } -const useStyles = makeStyles((theme: Theme) => ({ +const useStyles = makeStyles({ checkboxCell: { paddingLeft: 0 }, wideCell: { width: "100%" } -})); +}); const ProductVariantCreateAttributes: React.FC< ProductVariantCreateAttributesProps diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx index 9f66a7220..777e36632 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx @@ -2,6 +2,7 @@ import React from "react"; import { makeStyles } from "@material-ui/styles"; import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; +import { ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors } from "@saleor/products/types/ProductVariantBulkCreate"; import { isSelected } from "@saleor/utils/lists"; import { ProductVariantCreateFormData } from "./form"; import ProductVariantCreateAttributes from "./ProductVariantCreateAttributes"; @@ -24,6 +25,7 @@ export interface ProductVariantCreateContentProps { currencySymbol: string; data: ProductVariantCreateFormData; dispatchFormDataAction: React.Dispatch; + errors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[]; step: ProductVariantCreateStep; } @@ -35,6 +37,7 @@ const ProductVariantCreateContent: React.FC< currencySymbol, data, dispatchFormDataAction, + errors, step } = props; const classes = useStyles(props); @@ -121,6 +124,7 @@ const ProductVariantCreateContent: React.FC< attributes={selectedAttributes} currencySymbol={currencySymbol} data={data} + errors={errors} onVariantDataChange={(variantIndex, field, value) => dispatchFormDataAction({ field, diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx index a83903d54..a86601cc8 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx @@ -7,6 +7,8 @@ import { Theme } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/styles"; import React from "react"; import { FormattedMessage } from "react-intl"; + +import { ProductVariantBulkCreateInput } from "../../../types/globalTypes"; import { initialForm, ProductVariantCreateFormData } from "./form"; import ProductVariantCreateContent, { ProductVariantCreateContentProps @@ -73,17 +75,17 @@ function canHitNext( export interface ProductVariantCreateDialogProps extends Omit< ProductVariantCreateContentProps, - "dispatchFormDataAction" | "step" + "data" | "dispatchFormDataAction" | "step" > { open: boolean; - onClose: () => undefined; - onSubmit: (data: ProductVariantCreateFormData) => void; + onClose: () => void; + onSubmit: (data: ProductVariantBulkCreateInput[]) => void; } const ProductVariantCreateDialog: React.FC< ProductVariantCreateDialogProps > = props => { - const { open, onClose, ...contentProps } = props; + const { open, onClose, onSubmit, ...contentProps } = props; const classes = useStyles(props); const [step, setStep] = React.useState( "attributes" @@ -167,6 +169,7 @@ const ProductVariantCreateDialog: React.FC< color="primary" disabled={!canHitNext(step, data)} variant="contained" + onClick={() => onSubmit(data.variants)} > attribute.id === data.price.attribute).values + ? attributes + .find(attribute => attribute.id === data.price.attribute) + .values.filter(value => + data.attributes + .find(attribute => attribute.id === data.price.attribute) + .values.includes(value.slug) + ) : []; const stockAttributeValues = data.stock.all ? null : data.stock.attribute - ? attributes.find(attribute => attribute.id === data.stock.attribute).values + ? attributes + .find(attribute => attribute.id === data.stock.attribute) + .values.filter(value => + data.attributes + .find(attribute => attribute.id === data.stock.attribute) + .values.includes(value.slug) + ) : []; return ( @@ -158,7 +170,7 @@ const ProductVariantCreatePrices: React.FC< value={data.price.values[attributeValueIndex].value} onChange={event => onAttributeValueChange( - attributeValue.id, + attributeValue.slug, event.target.value, "price" ) @@ -256,7 +268,7 @@ const ProductVariantCreatePrices: React.FC< value={data.stock.values[attributeValueIndex].value} onChange={event => onAttributeValueChange( - attributeValue.id, + attributeValue.slug, event.target.value, "stock" ) diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx index 53c98d44a..1d74e48f1 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx @@ -4,11 +4,6 @@ import green from "@material-ui/core/colors/green"; import purple from "@material-ui/core/colors/purple"; import yellow from "@material-ui/core/colors/yellow"; import { Theme } from "@material-ui/core/styles"; -import Table from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; -import TableCell from "@material-ui/core/TableCell"; -import TableHead from "@material-ui/core/TableHead"; -import TableRow from "@material-ui/core/TableRow"; import TextField from "@material-ui/core/TextField"; import Typography from "@material-ui/core/Typography"; import { makeStyles } from "@material-ui/styles"; @@ -17,7 +12,9 @@ import React from "react"; import { FormattedMessage } from "react-intl"; import Hr from "@saleor/components/Hr"; -import { ProductVariantCreateInput } from "@saleor/types/globalTypes"; +import { maybe } from "@saleor/misc"; +import { ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors } from "@saleor/products/types/ProductVariantBulkCreate"; +import { ProductVariantBulkCreateInput } from "@saleor/types/globalTypes"; import { ProductDetails_product_productType_variantAttributes } from "../../types/ProductDetails"; import { ProductVariantCreateFormData } from "./form"; import { VariantField } from "./reducer"; @@ -26,6 +23,7 @@ export interface ProductVariantCreateSummaryProps { attributes: ProductDetails_product_productType_variantAttributes[]; currencySymbol: string; data: ProductVariantCreateFormData; + errors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[]; onVariantDataChange: ( variantIndex: number, field: VariantField, @@ -35,42 +33,58 @@ export interface ProductVariantCreateSummaryProps { const colors = [blue, cyan, green, purple, yellow].map(color => color[800]); -const useStyles = makeStyles((theme: Theme) => ({ - attributeValue: { - display: "inline-block", - marginRight: theme.spacing.unit - }, - col: { - paddingLeft: theme.spacing.unit, - paddingRight: theme.spacing.unit - }, - colName: { - paddingLeft: "0 !important", - width: "auto" - }, - colPrice: { - width: 200 - }, - colSku: { - width: 210 - }, - colStock: { - width: 120 - }, - hr: { - marginBottom: theme.spacing.unit, - marginTop: theme.spacing.unit / 2 - }, - input: { - "& input": { - padding: "16px 12px 17px" +const useStyles = makeStyles( + (theme: Theme) => ({ + attributeValue: { + display: "inline-block", + marginRight: theme.spacing.unit }, - marginTop: theme.spacing.unit / 2 + col: { + ...theme.typography.body2, + fontSize: 14, + paddingLeft: theme.spacing.unit, + paddingRight: theme.spacing.unit + }, + colHeader: { + ...theme.typography.body2, + fontSize: 14 + }, + colName: { + "&&": { + paddingLeft: "0 !important" + }, + "&:not($colHeader)": { + paddingTop: theme.spacing.unit * 2 + } + }, + colPrice: {}, + colSku: {}, + colStock: {}, + errorRow: {}, + hr: { + marginBottom: theme.spacing.unit, + marginTop: theme.spacing.unit / 2 + }, + input: { + "& input": { + padding: "16px 12px 17px" + }, + marginTop: theme.spacing.unit / 2 + }, + row: { + borderBottom: `1px solid ${theme.palette.divider}`, + display: "grid", + gridTemplateColumns: "1fr 200px 120px 210px", + padding: `${theme.spacing.unit}px 0` + } + }), + { + name: "ProductVariantCreateSummary" } -})); +); function getVariantName( - variant: ProductVariantCreateInput, + variant: ProductVariantBulkCreateInput, attributes: ProductDetails_product_productType_variantAttributes[] ): string[] { return attributes.reduce( @@ -78,7 +92,7 @@ function getVariantName( ...acc, attribute.values.find( value => - value.id === + value.slug === variant.attributes.find( variantAttribute => variantAttribute.id === attribute.id ).values[0] @@ -91,7 +105,13 @@ function getVariantName( const ProductVariantCreateSummary: React.FC< ProductVariantCreateSummaryProps > = props => { - const { attributes, currencySymbol, data, onVariantDataChange } = props; + const { + attributes, + currencySymbol, + data, + errors, + onVariantDataChange + } = props; const classes = useStyles(props); return ( @@ -103,40 +123,69 @@ const ProductVariantCreateSummary: React.FC< />
- - - - - - - - - - - - - - - - - - - {data.variants.map((variant, variantIndex) => ( - +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ {data.variants.map((variant, variantIndex) => { + const variantErrors = errors.filter( + error => error.index === variantIndex + ); + + return ( +
0 + })} key={variant.attributes .map(attribute => attribute.values[0]) .join(":")} > - +
{getVariantName(variant, attributes).map( (value, valueIndex) => ( ) )} - - - - onVariantDataChange( - variantIndex, - "stock", - event.target.value - ) - } - /> - - +
+
error.field === "priceOverride" + ) + } + helperText={maybe( + () => + variantErrors.find( + error => error.field === "priceOverride" + ).message + )} inputProps={{ min: 0, type: "number" @@ -188,21 +230,52 @@ const ProductVariantCreateSummary: React.FC< ) } /> - - +
+
error.field === "quantity") + } + helperText={maybe( + () => + variantErrors.find(error => error.field === "quantity") + .message + )} + inputProps={{ + min: 0, + type: "number" + }} + fullWidth + value={variant.quantity} + onChange={event => + onVariantDataChange( + variantIndex, + "stock", + event.target.value + ) + } + /> +
+
+ error.field === "sku")} + helperText={maybe( + () => + variantErrors.find(error => error.field === "sku").message + )} fullWidth value={variant.sku} onChange={event => onVariantDataChange(variantIndex, "sku", event.target.value) } /> - - - ))} - -
+
+ + ); + })} + ); }; diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx index 801dc222f..8d73cabe3 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx @@ -48,15 +48,15 @@ const ProductVariantCreateValues: React.FC< {attribute.values.map(value => ( attribute.id === dataAttribute.id ).values, (a, b) => a === b )} - name={`value:${value.id}`} + name={`value:${value.slug}`} label={value.name} - onChange={() => onValueClick(attribute.id, value.id)} + onChange={() => onValueClick(attribute.id, value.slug)} /> ))} diff --git a/src/products/components/ProductVariantCreateDialog/__snapshots__/reducer.test.ts.snap b/src/products/components/ProductVariantCreateDialog/__snapshots__/reducer.test.ts.snap index b40089ef1..f9b455627 100644 --- a/src/products/components/ProductVariantCreateDialog/__snapshots__/reducer.test.ts.snap +++ b/src/products/components/ProductVariantCreateDialog/__snapshots__/reducer.test.ts.snap @@ -133,7 +133,6 @@ Object { }, ], "priceOverride": "45.99", - "product": "", "quantity": NaN, "sku": "", }, @@ -159,7 +158,6 @@ Object { }, ], "priceOverride": "45.99", - "product": "", "quantity": NaN, "sku": "", }, @@ -185,7 +183,6 @@ Object { }, ], "priceOverride": "45.99", - "product": "", "quantity": NaN, "sku": "", }, @@ -211,7 +208,6 @@ Object { }, ], "priceOverride": "45.99", - "product": "", "quantity": NaN, "sku": "", }, @@ -237,7 +233,6 @@ Object { }, ], "priceOverride": "45.99", - "product": "", "quantity": NaN, "sku": "", }, @@ -263,7 +258,6 @@ Object { }, ], "priceOverride": "45.99", - "product": "", "quantity": NaN, "sku": "", }, @@ -289,7 +283,6 @@ Object { }, ], "priceOverride": "45.99", - "product": "", "quantity": NaN, "sku": "", }, @@ -315,7 +308,6 @@ Object { }, ], "priceOverride": "45.99", - "product": "", "quantity": NaN, "sku": "", }, @@ -354,11 +346,11 @@ Object { "value": "", "values": Array [ Object { - "id": "val-1-1", + "slug": "val-1-1", "value": "45.99", }, Object { - "id": "val-1-7", + "slug": "val-1-7", "value": "51.99", }, ], @@ -392,7 +384,6 @@ Object { }, ], "priceOverride": "45.99", - "product": "", "quantity": NaN, "sku": "", }, @@ -418,7 +409,6 @@ Object { }, ], "priceOverride": "45.99", - "product": "", "quantity": NaN, "sku": "", }, @@ -444,7 +434,6 @@ Object { }, ], "priceOverride": "45.99", - "product": "", "quantity": NaN, "sku": "", }, @@ -470,7 +459,6 @@ Object { }, ], "priceOverride": "45.99", - "product": "", "quantity": NaN, "sku": "", }, @@ -496,7 +484,6 @@ Object { }, ], "priceOverride": "51.99", - "product": "", "quantity": NaN, "sku": "", }, @@ -522,7 +509,6 @@ Object { }, ], "priceOverride": "51.99", - "product": "", "quantity": NaN, "sku": "", }, @@ -548,7 +534,6 @@ Object { }, ], "priceOverride": "51.99", - "product": "", "quantity": NaN, "sku": "", }, @@ -574,7 +559,6 @@ Object { }, ], "priceOverride": "51.99", - "product": "", "quantity": NaN, "sku": "", }, @@ -642,7 +626,6 @@ Object { }, ], "priceOverride": "", - "product": "", "quantity": 45, "sku": "", }, @@ -668,7 +651,6 @@ Object { }, ], "priceOverride": "", - "product": "", "quantity": 45, "sku": "", }, @@ -694,7 +676,6 @@ Object { }, ], "priceOverride": "", - "product": "", "quantity": 45, "sku": "", }, @@ -720,7 +701,6 @@ Object { }, ], "priceOverride": "", - "product": "", "quantity": 45, "sku": "", }, @@ -746,7 +726,6 @@ Object { }, ], "priceOverride": "", - "product": "", "quantity": 45, "sku": "", }, @@ -772,7 +751,6 @@ Object { }, ], "priceOverride": "", - "product": "", "quantity": 45, "sku": "", }, @@ -798,7 +776,6 @@ Object { }, ], "priceOverride": "", - "product": "", "quantity": 45, "sku": "", }, @@ -824,7 +801,6 @@ Object { }, ], "priceOverride": "", - "product": "", "quantity": 45, "sku": "", }, @@ -869,11 +845,11 @@ Object { "value": "", "values": Array [ Object { - "id": "val-1-1", + "slug": "val-1-1", "value": "13", }, Object { - "id": "val-1-7", + "slug": "val-1-7", "value": "19", }, ], @@ -901,7 +877,6 @@ Object { }, ], "priceOverride": "", - "product": "", "quantity": 13, "sku": "", }, @@ -927,7 +902,6 @@ Object { }, ], "priceOverride": "", - "product": "", "quantity": 13, "sku": "", }, @@ -953,7 +927,6 @@ Object { }, ], "priceOverride": "", - "product": "", "quantity": 13, "sku": "", }, @@ -979,7 +952,6 @@ Object { }, ], "priceOverride": "", - "product": "", "quantity": 13, "sku": "", }, @@ -1005,7 +977,6 @@ Object { }, ], "priceOverride": "", - "product": "", "quantity": 19, "sku": "", }, @@ -1031,7 +1002,6 @@ Object { }, ], "priceOverride": "", - "product": "", "quantity": 19, "sku": "", }, @@ -1057,7 +1027,6 @@ Object { }, ], "priceOverride": "", - "product": "", "quantity": 19, "sku": "", }, @@ -1083,7 +1052,6 @@ Object { }, ], "priceOverride": "", - "product": "", "quantity": 19, "sku": "", }, diff --git a/src/products/components/ProductVariantCreateDialog/createVariants.test.ts b/src/products/components/ProductVariantCreateDialog/createVariants.test.ts index 1ecc83b45..e41c65229 100644 --- a/src/products/components/ProductVariantCreateDialog/createVariants.test.ts +++ b/src/products/components/ProductVariantCreateDialog/createVariants.test.ts @@ -62,7 +62,7 @@ describe("Creates variant matrix", () => { all: false, attribute: attribute.id, values: attribute.values.map((attributeValue, attributeValueIndex) => ({ - id: attributeValue, + slug: attributeValue, value: (price * (attributeValueIndex + 1)).toString() })) }, @@ -120,7 +120,7 @@ describe("Creates variant matrix", () => { all: false, attribute: attribute.id, values: attribute.values.map((attributeValue, attributeValueIndex) => ({ - id: attributeValue, + slug: attributeValue, value: (stock * (attributeValueIndex + 1)).toString() })) } @@ -166,7 +166,7 @@ describe("Creates variant matrix", () => { all: false, attribute: attribute.id, values: attribute.values.map((attributeValue, attributeValueIndex) => ({ - id: attributeValue, + slug: attributeValue, value: (price * (attributeValueIndex + 1)).toString() })) }, @@ -175,7 +175,7 @@ describe("Creates variant matrix", () => { all: false, attribute: attribute.id, values: attribute.values.map((attributeValue, attributeValueIndex) => ({ - id: attributeValue, + slug: attributeValue, value: (stock * (attributeValueIndex + 1)).toString() })) } diff --git a/src/products/components/ProductVariantCreateDialog/createVariants.ts b/src/products/components/ProductVariantCreateDialog/createVariants.ts index 4433aa5ef..6239832e2 100644 --- a/src/products/components/ProductVariantCreateDialog/createVariants.ts +++ b/src/products/components/ProductVariantCreateDialog/createVariants.ts @@ -1,4 +1,4 @@ -import { ProductVariantCreateInput } from "@saleor/types/globalTypes"; +import { ProductVariantBulkCreateInput } from "@saleor/types/globalTypes"; import { AllOrAttribute, Attribute, @@ -7,7 +7,7 @@ import { interface CreateVariantAttributeValueInput { attributeId: string; - attributeValueId: string; + attributeValueSlug: string; } type CreateVariantInput = CreateVariantAttributeValueInput[]; @@ -20,7 +20,7 @@ function getAttributeValuePriceOrStock( ); const attributeValue = priceOrStock.values.find( - attributeValue => attribute.attributeValueId === attributeValue.id + attributeValue => attribute.attributeValueSlug === attributeValue.slug ); return attributeValue.value; @@ -29,7 +29,7 @@ function getAttributeValuePriceOrStock( function createVariant( data: ProductVariantCreateFormData, attributes: CreateVariantInput -): ProductVariantCreateInput { +): ProductVariantBulkCreateInput { const priceOverride = data.price.all ? data.price.value : getAttributeValuePriceOrStock(attributes, data.price); @@ -43,10 +43,9 @@ function createVariant( return { attributes: attributes.map(attribute => ({ id: attribute.attributeId, - values: [attribute.attributeValueId] + values: [attribute.attributeValueSlug] })), priceOverride, - product: "", quantity, sku: "" }; @@ -56,11 +55,11 @@ function addAttributeToVariant( attribute: Attribute, variant: CreateVariantInput ): CreateVariantInput[] { - return attribute.values.map(attributeValueId => [ + return attribute.values.map(attributeValueSlug => [ ...variant, { attributeId: attribute.id, - attributeValueId + attributeValueSlug } ]); } @@ -91,7 +90,7 @@ export function createVariantFlatMatrixDimension( export function createVariants( data: ProductVariantCreateFormData -): ProductVariantCreateInput[] { +): ProductVariantBulkCreateInput[] { if ( (!data.price.all && !data.price.attribute) || (!data.stock.all && !data.stock.attribute) diff --git a/src/products/components/ProductVariantCreateDialog/fixtures.ts b/src/products/components/ProductVariantCreateDialog/fixtures.ts index 9601839f0..e53aa0aca 100644 --- a/src/products/components/ProductVariantCreateDialog/fixtures.ts +++ b/src/products/components/ProductVariantCreateDialog/fixtures.ts @@ -74,11 +74,11 @@ const price: AllOrAttribute = { value: "", values: [ { - id: thirdStep.attributes[1].values[0], + slug: thirdStep.attributes[1].values[0], value: "24.99" }, { - id: thirdStep.attributes[1].values[1], + slug: thirdStep.attributes[1].values[1], value: "26.99" } ] @@ -89,11 +89,11 @@ const stock: AllOrAttribute = { value: "", values: [ { - id: thirdStep.attributes[2].values[0], + slug: thirdStep.attributes[2].values[0], value: "50" }, { - id: thirdStep.attributes[2].values[1], + slug: thirdStep.attributes[2].values[1], value: "35" } ] diff --git a/src/products/components/ProductVariantCreateDialog/form.ts b/src/products/components/ProductVariantCreateDialog/form.ts index 157ad2c55..b6331224e 100644 --- a/src/products/components/ProductVariantCreateDialog/form.ts +++ b/src/products/components/ProductVariantCreateDialog/form.ts @@ -1,7 +1,7 @@ -import { ProductVariantCreateInput } from "../../../types/globalTypes"; +import { ProductVariantBulkCreateInput } from "../../../types/globalTypes"; export interface AttributeValue { - id: string; + slug: string; value: string; } export interface AllOrAttribute { @@ -18,7 +18,7 @@ export interface ProductVariantCreateFormData { attributes: Attribute[]; price: AllOrAttribute; stock: AllOrAttribute; - variants: ProductVariantCreateInput[]; + variants: ProductVariantBulkCreateInput[]; } export const initialForm: ProductVariantCreateFormData = { diff --git a/src/products/components/ProductVariantCreateDialog/reducer.ts b/src/products/components/ProductVariantCreateDialog/reducer.ts index 045685921..f1492ed22 100644 --- a/src/products/components/ProductVariantCreateDialog/reducer.ts +++ b/src/products/components/ProductVariantCreateDialog/reducer.ts @@ -108,20 +108,20 @@ function applyStockToAll( function changeAttributeValuePrice( state: ProductVariantCreateFormData, - attributeValueId: string, + attributeValueSlug: string, price: string ): ProductVariantCreateFormData { const index = state.price.values.findIndex( - value => value.id === attributeValueId + value => value.slug === attributeValueSlug ); if (index === -1) { - throw new Error(`Value with id ${attributeValueId} not found`); + throw new Error(`Value with id ${attributeValueSlug} not found`); } const values = updateAtIndex( { - id: attributeValueId, + slug: attributeValueSlug, value: price }, state.price.values, @@ -144,20 +144,20 @@ function changeAttributeValuePrice( function changeAttributeValueStock( state: ProductVariantCreateFormData, - attributeValueId: string, + attributeValueSlug: string, stock: string ): ProductVariantCreateFormData { const index = state.stock.values.findIndex( - value => value.id === attributeValueId + value => value.slug === attributeValueSlug ); if (index === -1) { - throw new Error(`Value with id ${attributeValueId} not found`); + throw new Error(`Value with id ${attributeValueSlug} not found`); } const values = updateAtIndex( { - id: attributeValueId, + slug: attributeValueSlug, value: stock }, state.stock.values, @@ -185,8 +185,8 @@ function changeApplyPriceToAttributeId( const attribute = state.attributes.find( attribute => attribute.id === attributeId ); - const values = attribute.values.map(id => ({ - id, + const values = attribute.values.map(slug => ({ + slug, value: "" })); const data = { @@ -211,8 +211,8 @@ function changeApplyStockToAttributeId( const attribute = state.attributes.find( attribute => attribute.id === attributeId ); - const values = attribute.values.map(id => ({ - id, + const values = attribute.values.map(slug => ({ + slug, value: "" })); diff --git a/src/products/components/ProductVariants/ProductVariants.tsx b/src/products/components/ProductVariants/ProductVariants.tsx index bf6332396..0de9f974d 100644 --- a/src/products/components/ProductVariants/ProductVariants.tsx +++ b/src/products/components/ProductVariants/ProductVariants.tsx @@ -69,6 +69,7 @@ interface ProductVariantsProps extends ListActions, WithStyles { fallbackPrice?: ProductVariant_costPrice; onRowClick: (id: string) => () => void; onVariantAdd?(); + onVariantsAdd?(); } const numberOfColumns = 5; @@ -81,6 +82,7 @@ export const ProductVariants = withStyles(styles, { name: "ProductVariants" })( fallbackPrice, onRowClick, onVariantAdd, + onVariantsAdd, isChecked, selected, toggle, @@ -98,7 +100,7 @@ export const ProductVariants = withStyles(styles, { name: "ProductVariants" })( description: "section header" })} toolbar={ - <> + hasVariants ? ( - + ) : ( + + ) } /> {!variants.length && ( diff --git a/src/products/containers/ProductUpdateOperations.tsx b/src/products/containers/ProductUpdateOperations.tsx index 3ed040bbc..73940d8e5 100644 --- a/src/products/containers/ProductUpdateOperations.tsx +++ b/src/products/containers/ProductUpdateOperations.tsx @@ -7,6 +7,7 @@ import { TypedProductImageCreateMutation, TypedProductImageDeleteMutation, TypedProductUpdateMutation, + TypedProductVariantBulkCreateMutation, TypedProductVariantBulkDeleteMutation, TypedSimpleProductUpdateMutation } from "../mutations"; @@ -25,6 +26,10 @@ import { ProductImageReorderVariables } from "../types/ProductImageReorder"; import { ProductUpdate, ProductUpdateVariables } from "../types/ProductUpdate"; +import { + ProductVariantBulkCreate, + ProductVariantBulkCreateVariables +} from "../types/ProductVariantBulkCreate"; import { ProductVariantBulkDelete, ProductVariantBulkDeleteVariables @@ -38,6 +43,10 @@ import ProductImagesReorderProvider from "./ProductImagesReorder"; interface ProductUpdateOperationsProps { product: ProductDetails_product; children: (props: { + bulkProductVariantCreate: PartialMutationProviderOutput< + ProductVariantBulkCreate, + ProductVariantBulkCreateVariables + >; bulkProductVariantDelete: PartialMutationProviderOutput< ProductVariantBulkDelete, ProductVariantBulkDeleteVariables @@ -67,6 +76,7 @@ interface ProductUpdateOperationsProps { SimpleProductUpdateVariables >; }) => React.ReactNode; + onBulkProductVariantCreate?: (data: ProductVariantBulkCreate) => void; onBulkProductVariantDelete?: (data: ProductVariantBulkDelete) => void; onDelete?: (data: ProductDelete) => void; onImageCreate?: (data: ProductImageCreate) => void; @@ -80,6 +90,7 @@ const ProductUpdateOperations: React.StatelessComponent< > = ({ product, children, + onBulkProductVariantCreate, onBulkProductVariantDelete, onDelete, onImageDelete, @@ -112,31 +123,40 @@ const ProductUpdateOperations: React.StatelessComponent< - {(...bulkProductVariantDelete) => - children({ - bulkProductVariantDelete: getMutationProviderData( - ...bulkProductVariantDelete - ), - createProductImage: getMutationProviderData( - ...createProductImage - ), - deleteProduct: getMutationProviderData( - ...deleteProduct - ), - deleteProductImage: getMutationProviderData( - ...deleteProductImage - ), - reorderProductImages: getMutationProviderData( - ...reorderProductImages - ), - updateProduct: getMutationProviderData( - ...updateProduct - ), - updateSimpleProduct: getMutationProviderData( - ...updateSimpleProduct - ) - }) - } + {(...bulkProductVariantDelete) => ( + + {(...bulkProductVariantCreate) => + children({ + bulkProductVariantCreate: getMutationProviderData( + ...bulkProductVariantCreate + ), + bulkProductVariantDelete: getMutationProviderData( + ...bulkProductVariantDelete + ), + createProductImage: getMutationProviderData( + ...createProductImage + ), + deleteProduct: getMutationProviderData( + ...deleteProduct + ), + deleteProductImage: getMutationProviderData( + ...deleteProductImage + ), + reorderProductImages: getMutationProviderData( + ...reorderProductImages + ), + updateProduct: getMutationProviderData( + ...updateProduct + ), + updateSimpleProduct: getMutationProviderData( + ...updateSimpleProduct + ) + }) + } + + )} )} diff --git a/src/products/mutations.ts b/src/products/mutations.ts index 563c09f84..c612d576d 100644 --- a/src/products/mutations.ts +++ b/src/products/mutations.ts @@ -45,6 +45,10 @@ import { productBulkPublish, productBulkPublishVariables } from "./types/productBulkPublish"; +import { + ProductVariantBulkCreate, + ProductVariantBulkCreateVariables +} from "./types/ProductVariantBulkCreate"; import { ProductVariantBulkDelete, ProductVariantBulkDeleteVariables @@ -440,6 +444,30 @@ export const TypedProductBulkPublishMutation = TypedMutation< productBulkPublishVariables >(productBulkPublishMutation); +export const ProductVariantBulkCreateMutation = gql` + mutation ProductVariantBulkCreate( + $id: ID! + $inputs: [ProductVariantBulkCreateInput]! + ) { + productVariantBulkCreate(product: $id, variants: $inputs) { + bulkProductErrors { + field + message + code + index + } + errors { + field + message + } + } + } +`; +export const TypedProductVariantBulkCreateMutation = TypedMutation< + ProductVariantBulkCreate, + ProductVariantBulkCreateVariables +>(ProductVariantBulkCreateMutation); + export const ProductVariantBulkDeleteMutation = gql` mutation ProductVariantBulkDelete($ids: [ID!]!) { productVariantBulkDelete(ids: $ids) { diff --git a/src/products/queries.ts b/src/products/queries.ts index efbdb9489..626b6ff3a 100644 --- a/src/products/queries.ts +++ b/src/products/queries.ts @@ -267,6 +267,7 @@ const productDetailsQuery = gql` values { id name + slug } } } diff --git a/src/products/types/ProductDetails.ts b/src/products/types/ProductDetails.ts index 803e66d8d..3ed43c47a 100644 --- a/src/products/types/ProductDetails.ts +++ b/src/products/types/ProductDetails.ts @@ -143,6 +143,7 @@ export interface ProductDetails_product_productType_variantAttributes_values { __typename: "AttributeValue"; id: string; name: string | null; + slug: string | null; } export interface ProductDetails_product_productType_variantAttributes { diff --git a/src/products/types/ProductVariantBulkCreate.ts b/src/products/types/ProductVariantBulkCreate.ts new file mode 100644 index 000000000..f0a0160ff --- /dev/null +++ b/src/products/types/ProductVariantBulkCreate.ts @@ -0,0 +1,38 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { ProductVariantBulkCreateInput, ProductErrorCode } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL mutation operation: ProductVariantBulkCreate +// ==================================================== + +export interface ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors { + __typename: "BulkProductError"; + field: string | null; + message: string | null; + code: ProductErrorCode | null; + index: number | null; +} + +export interface ProductVariantBulkCreate_productVariantBulkCreate_errors { + __typename: "Error"; + field: string | null; + message: string | null; +} + +export interface ProductVariantBulkCreate_productVariantBulkCreate { + __typename: "ProductVariantBulkCreate"; + bulkProductErrors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[] | null; + errors: ProductVariantBulkCreate_productVariantBulkCreate_errors[] | null; +} + +export interface ProductVariantBulkCreate { + productVariantBulkCreate: ProductVariantBulkCreate_productVariantBulkCreate | null; +} + +export interface ProductVariantBulkCreateVariables { + id: string; + inputs: (ProductVariantBulkCreateInput | null)[]; +} diff --git a/src/products/urls.ts b/src/products/urls.ts index 5ebfe8d31..2f240496f 100644 --- a/src/products/urls.ts +++ b/src/products/urls.ts @@ -53,7 +53,7 @@ export const productListUrl = (params?: ProductListUrlQueryParams): string => export const productPath = (id: string) => urlJoin(productSection + id); export type ProductUrlDialog = "remove"; export type ProductUrlQueryParams = BulkAction & - Dialog<"remove" | "remove-variants">; + Dialog<"create-variants" | "remove" | "remove-variants">; export const productUrl = (id: string, params?: ProductUrlQueryParams) => productPath(encodeURIComponent(id)) + "?" + stringifyQs(params); diff --git a/src/products/views/ProductUpdate/ProductUpdate.tsx b/src/products/views/ProductUpdate/ProductUpdate.tsx index d86c58c25..c12d53bc3 100644 --- a/src/products/views/ProductUpdate/ProductUpdate.tsx +++ b/src/products/views/ProductUpdate/ProductUpdate.tsx @@ -10,7 +10,10 @@ import { WindowTitle } from "@saleor/components/WindowTitle"; import useBulkActions from "@saleor/hooks/useBulkActions"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; +import useShop from "@saleor/hooks/useShop"; import { commonMessages } from "@saleor/intl"; +import ProductVariantCreateDialog from "@saleor/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog"; +import { ProductVariantBulkCreate } from "@saleor/products/types/ProductVariantBulkCreate"; import { DEFAULT_INITIAL_SEARCH_DATA } from "../../../config"; import SearchCategories from "../../../containers/SearchCategories"; import SearchCollections from "../../../containers/SearchCollections"; @@ -54,6 +57,7 @@ export const ProductUpdate: React.StatelessComponent = ({ params.ids ); const intl = useIntl(); + const shop = useShop(); const openModal = (action: ProductUrlDialog) => navigate( @@ -115,6 +119,15 @@ export const ProductUpdate: React.StatelessComponent = ({ const handleVariantAdd = () => navigate(productVariantAddUrl(id)); + const handleBulkProductVariantCreate = ( + data: ProductVariantBulkCreate + ) => { + if (data.productVariantBulkCreate.errors.length === 0) { + navigate(productUrl(id), true); + refetch(); + } + }; + const handleBulkProductVariantDelete = ( data: ProductVariantBulkDelete ) => { @@ -125,10 +138,19 @@ export const ProductUpdate: React.StatelessComponent = ({ } }; + const handleVariantCreatorOpen = () => + navigate( + productUrl(id, { + ...params, + action: "create-variants" + }) + ); + const product = data ? data.product : undefined; return ( = ({ onUpdate={handleUpdate} > {({ + bulkProductVariantCreate, bulkProductVariantDelete, createProductImage, deleteProduct, @@ -245,6 +268,7 @@ export const ProductUpdate: React.StatelessComponent = ({ onImageReorder={handleImageReorder} onSubmit={handleSubmit} onVariantAdd={handleVariantAdd} + onVariantsAdd={handleVariantCreatorOpen} onVariantShow={variantId => () => navigate( productVariantEditUrl(product.id, variantId) @@ -328,6 +352,34 @@ export const ProductUpdate: React.StatelessComponent = ({ /> + + bulkProductVariantCreate.opts.data + .productVariantBulkCreate.bulkProductErrors, + [] + )} + open={params.action === "create-variants"} + attributes={maybe( + () => data.product.productType.variantAttributes, + [] + )} + currencySymbol={maybe(() => shop.defaultCurrency)} + onClose={() => + navigate( + productUrl(id, { + ...params, + action: undefined + }) + ) + } + onSubmit={inputs => + bulkProductVariantCreate.mutate({ + id, + inputs + }) + } + /> ); }} diff --git a/src/products/views/ProductVariantCreate.tsx b/src/products/views/ProductVariantCreate.tsx index b78ca66a9..a8ac30785 100644 --- a/src/products/views/ProductVariantCreate.tsx +++ b/src/products/views/ProductVariantCreate.tsx @@ -58,18 +58,20 @@ export const ProductVariant: React.StatelessComponent = ({ ) => variantCreate({ variables: { - attributes: formData.attributes - .filter(attribute => attribute.value !== "") - .map(attribute => ({ - id: attribute.id, - values: [attribute.value] - })), - costPrice: decimal(formData.costPrice), - priceOverride: decimal(formData.priceOverride), - product: productId, - quantity: formData.quantity || null, - sku: formData.sku, - trackInventory: true + input: { + attributes: formData.attributes + .filter(attribute => attribute.value !== "") + .map(attribute => ({ + id: attribute.id, + values: [attribute.value] + })), + costPrice: decimal(formData.costPrice), + priceOverride: decimal(formData.priceOverride), + product: productId, + quantity: formData.quantity || null, + sku: formData.sku, + trackInventory: true + } } }); const handleVariantClick = (id: string) => diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index 8fbbd2cc5..3c6fd9f16 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -82051,6 +82051,1203 @@ exports[`Storyshots Views / Product types / Unassign multiple attributes default /> `; +exports[`Storyshots Views / Products / Create multiple variants / summary default 1`] = ` +
+
+
+
+
+
+ + Choose Attributes + +
+
+ + Select Values + +
+
+ + Prices and SKU + +
+
+ + Summary + +
+
+
+
+ You will create variants below +
+
+
+
+
+ Variant +
+
+ Price +
+
+ Inventory +
+
+ SKU +
+
+
+
+ + 100g + + + Arabica + + + Round + +
+
+
+
+ + + USD +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + 100g + + + Arabica + + + Polo + +
+
+
+
+ + + USD +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + 500g + + + Arabica + + + Round + +
+
+
+
+ + + USD +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + 500g + + + Arabica + + + Polo + +
+
+
+
+ + + USD +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+`; + +exports[`Storyshots Views / Products / Create multiple variants / summary errors 1`] = ` +
+
+
+
+
+
+ + Choose Attributes + +
+
+ + Select Values + +
+
+ + Prices and SKU + +
+
+ + Summary + +
+
+
+
+ You will create variants below +
+
+
+
+
+ Variant +
+
+ Price +
+
+ Inventory +
+
+ SKU +
+
+
+
+ + 100g + + + Arabica + + + Round + +
+
+
+
+ + + USD +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + 100g + + + Arabica + + + Polo + +
+
+
+
+ + + USD +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + 500g + + + Arabica + + + Round + +
+
+
+
+ + + USD +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + 500g + + + Arabica + + + Polo + +
+
+
+
+ + + USD +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+

+ Duplicated SKU. +

+
+
+
+
+
+
+
+
+
+`; + exports[`Storyshots Views / Products / Create multiple variants choose attributes 1`] = `
-
-
-

- Robusta -

-
-
-
- -
- - -
-
-
-
-
@@ -83019,58 +84164,6 @@ exports[`Storyshots Views / Products / Create multiple variants prices and SKU 1
-
-
-
-

- Robusta -

-
-
-
- -
- - -
-
-
-
@@ -83154,7 +84247,7 @@ exports[`Storyshots Views / Products / Create multiple variants select values 1` > @@ -83175,7 +84268,7 @@ exports[`Storyshots Views / Products / Create multiple variants select values 1` > @@ -83196,7 +84289,7 @@ exports[`Storyshots Views / Products / Create multiple variants select values 1` > @@ -83217,7 +84310,7 @@ exports[`Storyshots Views / Products / Create multiple variants select values 1` > @@ -83250,7 +84343,7 @@ exports[`Storyshots Views / Products / Create multiple variants select values 1` > @@ -83271,7 +84364,7 @@ exports[`Storyshots Views / Products / Create multiple variants select values 1` > @@ -83304,7 +84397,7 @@ exports[`Storyshots Views / Products / Create multiple variants select values 1` > @@ -83325,7 +84418,7 @@ exports[`Storyshots Views / Products / Create multiple variants select values 1` > @@ -83346,7 +84439,7 @@ exports[`Storyshots Views / Products / Create multiple variants select values 1` > @@ -83365,616 +84458,6 @@ exports[`Storyshots Views / Products / Create multiple variants select values 1` `; -exports[`Storyshots Views / Products / Create multiple variants summary 1`] = ` -
-
-
-
-
-
- - Choose Attributes - -
-
- - Select Values - -
-
- - Prices and SKU - -
-
- - Summary - -
-
-
-
- You will create variants below -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Variant - - Inventory - - Price - - SKU -
- - 100g - - - Arabica - - - Round - - -
-
- - -
-
-
-
-
- - - USD -
-
-
-
-
- - -
-
-
- - 100g - - - Arabica - - - Polo - - -
-
- - -
-
-
-
-
- - - USD -
-
-
-
-
- - -
-
-
- - 500g - - - Arabica - - - Round - - -
-
- - -
-
-
-
-
- - - USD -
-
-
-
-
- - -
-
-
- - 500g - - - Arabica - - - Polo - - -
-
- - -
-
-
-
-
- - - USD -
-
-
-
-
- - -
-
-
-
-
-
-
-
-`; - exports[`Storyshots Views / Products / Create product When loading 1`] = `
undefined, onVariantAdd: () => undefined, onVariantShow: () => undefined, + onVariantsAdd: () => undefined, placeholderImage, product, saveButtonBarState: "default", diff --git a/src/types/globalTypes.ts b/src/types/globalTypes.ts index 5dff42bb4..0c23293c3 100644 --- a/src/types/globalTypes.ts +++ b/src/types/globalTypes.ts @@ -193,6 +193,20 @@ export enum PermissionEnum { MANAGE_WEBHOOKS = "MANAGE_WEBHOOKS", } +export enum ProductErrorCode { + ALREADY_EXISTS = "ALREADY_EXISTS", + ATTRIBUTE_ALREADY_ASSIGNED = "ATTRIBUTE_ALREADY_ASSIGNED", + ATTRIBUTE_CANNOT_BE_ASSIGNED = "ATTRIBUTE_CANNOT_BE_ASSIGNED", + ATTRIBUTE_VARIANTS_DISABLED = "ATTRIBUTE_VARIANTS_DISABLED", + GRAPHQL_ERROR = "GRAPHQL_ERROR", + INVALID = "INVALID", + NOT_FOUND = "NOT_FOUND", + NOT_PRODUCTS_IMAGE = "NOT_PRODUCTS_IMAGE", + REQUIRED = "REQUIRED", + UNIQUE = "UNIQUE", + VARIANT_NO_DIGITAL_CONTENT = "VARIANT_NO_DIGITAL_CONTENT", +} + export enum ProductOrderField { DATE = "DATE", MINIMAL_PRICE = "MINIMAL_PRICE", @@ -614,6 +628,16 @@ export interface ProductTypeInput { taxCode?: string | null; } +export interface ProductVariantBulkCreateInput { + attributes: (AttributeValueInput | null)[]; + costPrice?: any | null; + priceOverride?: any | null; + sku: string; + quantity?: number | null; + trackInventory?: boolean | null; + weight?: any | null; +} + export interface ProductVariantCreateInput { attributes: (AttributeValueInput | null)[]; costPrice?: any | null; From 9b9a6648cdaf59d44f1d3347ef2e02ac3690df00 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Wed, 2 Oct 2019 16:29:22 +0200 Subject: [PATCH 14/32] Allow variant deleting --- .../ProductVariantCreateContent.tsx | 6 + .../ProductVariantCreateSummary.tsx | 20 +- .../ProductVariantCreateTabs.tsx | 1 + .../reducer.test.ts | 13 + .../ProductVariantCreateDialog/reducer.ts | 21 +- .../__snapshots__/Stories.test.ts.snap | 232 ++++++++++++++++++ 6 files changed, 290 insertions(+), 3 deletions(-) diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx index 777e36632..d451c1a75 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx @@ -133,6 +133,12 @@ const ProductVariantCreateContent: React.FC< variantIndex }) } + onVariantDelete={variantIndex => + dispatchFormDataAction({ + type: "deleteVariant", + variantIndex + }) + } /> )}
diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx index 1d74e48f1..0dbf89d26 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateSummary.tsx @@ -3,9 +3,11 @@ import cyan from "@material-ui/core/colors/cyan"; import green from "@material-ui/core/colors/green"; import purple from "@material-ui/core/colors/purple"; import yellow from "@material-ui/core/colors/yellow"; +import IconButton from "@material-ui/core/IconButton"; import { Theme } from "@material-ui/core/styles"; import TextField from "@material-ui/core/TextField"; import Typography from "@material-ui/core/Typography"; +import DeleteIcon from "@material-ui/icons/Delete"; import { makeStyles } from "@material-ui/styles"; import classNames from "classnames"; import React from "react"; @@ -29,6 +31,7 @@ export interface ProductVariantCreateSummaryProps { field: VariantField, value: string ) => void; + onVariantDelete: (variantIndex: number) => void; } const colors = [blue, cyan, green, purple, yellow].map(color => color[800]); @@ -60,6 +63,9 @@ const useStyles = makeStyles( colPrice: {}, colSku: {}, colStock: {}, + delete: { + marginTop: theme.spacing.unit / 2 + }, errorRow: {}, hr: { marginBottom: theme.spacing.unit, @@ -74,7 +80,7 @@ const useStyles = makeStyles( row: { borderBottom: `1px solid ${theme.palette.divider}`, display: "grid", - gridTemplateColumns: "1fr 200px 120px 210px", + gridTemplateColumns: "1fr 180px 120px 180px 64px", padding: `${theme.spacing.unit}px 0` } }), @@ -110,7 +116,8 @@ const ProductVariantCreateSummary: React.FC< currencySymbol, data, errors, - onVariantDataChange + onVariantDataChange, + onVariantDelete } = props; const classes = useStyles(props); @@ -272,6 +279,15 @@ const ProductVariantCreateSummary: React.FC< } /> +
+ onVariantDelete(variantIndex)} + > + + +
); })} diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.tsx index 11a57da01..94115a6a2 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.tsx @@ -93,6 +93,7 @@ const ProductVariantCreateTabs: React.FC< [classes.tabUnderline]: steps.findIndex(step => step.value === currentStep) >= stepIndex })} + key={step.value} > {step.label} diff --git a/src/products/components/ProductVariantCreateDialog/reducer.test.ts b/src/products/components/ProductVariantCreateDialog/reducer.test.ts index 5643c9816..39fd9395d 100644 --- a/src/products/components/ProductVariantCreateDialog/reducer.test.ts +++ b/src/products/components/ProductVariantCreateDialog/reducer.test.ts @@ -190,4 +190,17 @@ describe("Reducer is able to", () => { fourthStep.variants[variantIndex - 1].priceOverride ); }); + + it("delete variant", () => { + const variantIndex = 3; + + const state = execActions(fourthStep, reducer, [ + { + type: "deleteVariant", + variantIndex + } + ]); + + expect(state.variants.length).toBe(fourthStep.variants.length - 1); + }); }); diff --git a/src/products/components/ProductVariantCreateDialog/reducer.ts b/src/products/components/ProductVariantCreateDialog/reducer.ts index f1492ed22..367a69320 100644 --- a/src/products/components/ProductVariantCreateDialog/reducer.ts +++ b/src/products/components/ProductVariantCreateDialog/reducer.ts @@ -1,4 +1,10 @@ -import { add, remove, toggle, updateAtIndex } from "@saleor/utils/lists"; +import { + add, + remove, + toggle, + updateAtIndex, + removeAtIndex +} from "@saleor/utils/lists"; import { createVariants } from "./createVariants"; import { initialForm, ProductVariantCreateFormData } from "./form"; @@ -14,6 +20,7 @@ export type ProductVariantCreateReducerActionType = | "changeAttributeValuePrice" | "changeAttributeValueStock" | "changeVariantData" + | "deleteVariant" | "selectAttribute" | "selectValue"; @@ -288,6 +295,16 @@ function changeVariantData( }; } +function deleteVariant( + state: ProductVariantCreateFormData, + variantIndex: number +): ProductVariantCreateFormData { + return { + ...state, + variants: removeAtIndex(state.variants, variantIndex) + }; +} + function reduceProductVariantCreateFormData( prevState: ProductVariantCreateFormData, action: ProductVariantCreateReducerAction @@ -322,6 +339,8 @@ function reduceProductVariantCreateFormData( action.value, action.variantIndex ); + case "deleteVariant": + return deleteVariant(prevState, action.variantIndex); default: return prevState; } diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index 3c6fd9f16..a1f38c0cc 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -82263,6 +82263,35 @@ exports[`Storyshots Views / Products / Create multiple variants / summary defaul +
+ +
+
+ +
+
+ +
+
+ +
@@ -82859,6 +82975,35 @@ exports[`Storyshots Views / Products / Create multiple variants / summary errors +
+ +
+
+ +
+
+ +
+
+ +
From 0148bc452703d294717a4b1e1565cee3e95166a7 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 3 Oct 2019 11:02:37 +0200 Subject: [PATCH 15/32] Fix build warnings --- .../ProductVariantCreatePrices.tsx | 4 ++-- .../ProductVariantCreateSummary.tsx | 2 +- .../ProductVariantCreateValues.tsx | 7 ++++--- .../components/ProductVariantCreateDialog/reducer.ts | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx index 2b9148375..480fbfa6b 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx @@ -85,7 +85,7 @@ const ProductVariantCreatePrices: React.FC< return ( <> - + )} - + - + {attributes.map(attribute => ( - <> - + + {maybe(() => attribute.name, )}
@@ -57,10 +57,11 @@ const ProductVariantCreateValues: React.FC< name={`value:${value.slug}`} label={value.name} onChange={() => onValueClick(attribute.id, value.slug)} + key={value.slug} /> ))} - +
))} ); diff --git a/src/products/components/ProductVariantCreateDialog/reducer.ts b/src/products/components/ProductVariantCreateDialog/reducer.ts index 367a69320..12d9de6f5 100644 --- a/src/products/components/ProductVariantCreateDialog/reducer.ts +++ b/src/products/components/ProductVariantCreateDialog/reducer.ts @@ -1,9 +1,9 @@ import { add, remove, + removeAtIndex, toggle, - updateAtIndex, - removeAtIndex + updateAtIndex } from "@saleor/utils/lists"; import { createVariants } from "./createVariants"; import { initialForm, ProductVariantCreateFormData } from "./form"; From 36396c9d3ebd040d7774c633014d3ff5e5e3d060 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 3 Oct 2019 16:49:58 +0200 Subject: [PATCH 16/32] Remove attribute selection --- .../ProductVariantCreateAttributes.tsx | 77 ------------------- .../ProductVariantCreateContent.tsx | 13 ---- .../ProductVariantCreateDialog.tsx | 38 +++++---- .../ProductVariantCreateTabs.tsx | 7 -- .../ProductVariantCreateDialog/form.ts | 15 +++- .../ProductVariantCreateDialog/reducer.ts | 37 +++------ .../ProductVariantCreateDialog/types.ts | 6 +- .../views/ProductUpdate/ProductUpdate.tsx | 3 + 8 files changed, 47 insertions(+), 149 deletions(-) delete mode 100644 src/products/components/ProductVariantCreateDialog/ProductVariantCreateAttributes.tsx diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateAttributes.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateAttributes.tsx deleted file mode 100644 index 48b658d9f..000000000 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateAttributes.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import Table from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; -import TableCell from "@material-ui/core/TableCell"; -import TableRow from "@material-ui/core/TableRow"; -import makeStyles from "@material-ui/styles/makeStyles"; -import React from "react"; -import { FormattedMessage } from "react-intl"; - -import Checkbox from "@saleor/components/Checkbox"; -import { maybe, renderCollection } from "@saleor/misc"; -import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; -import { ProductVariantCreateFormData } from "./form"; - -export interface ProductVariantCreateAttributesProps { - attributes: ProductDetails_product_productType_variantAttributes[]; - data: ProductVariantCreateFormData; - onAttributeClick: (id: string) => void; -} - -const useStyles = makeStyles({ - checkboxCell: { - paddingLeft: 0 - }, - wideCell: { - width: "100%" - } -}); - -const ProductVariantCreateAttributes: React.FC< - ProductVariantCreateAttributesProps -> = props => { - const { attributes, data, onAttributeClick } = props; - const classes = useStyles(props); - - return ( - - - {renderCollection( - attributes, - attribute => { - if (!attribute) { - return null; - } - const isChecked = !!data.attributes.find( - selectedAttribute => selectedAttribute.id === attribute.id - ); - - return ( - attribute.id)}> - - onAttributeClick(attribute.id)} - /> - - - {attribute.name} - - - ); - }, - () => ( - - - - - - ) - )} - -
- ); -}; - -ProductVariantCreateAttributes.displayName = "ProductVariantCreateAttributes"; -export default ProductVariantCreateAttributes; diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx index d451c1a75..ef044632a 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx @@ -5,7 +5,6 @@ import { ProductDetails_product_productType_variantAttributes } from "@saleor/pr import { ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors } from "@saleor/products/types/ProductVariantBulkCreate"; import { isSelected } from "@saleor/utils/lists"; import { ProductVariantCreateFormData } from "./form"; -import ProductVariantCreateAttributes from "./ProductVariantCreateAttributes"; import ProductVariantCreatePrices from "./ProductVariantCreatePrices"; import ProductVariantCreateSummary from "./ProductVariantCreateSummary"; import ProductVariantCreateTabs from "./ProductVariantCreateTabs"; @@ -54,18 +53,6 @@ const ProductVariantCreateContent: React.FC<
- {step === "attributes" && ( - - dispatchFormDataAction({ - attributeId, - type: "selectAttribute" - }) - } - /> - )} {step === "values" && ( 0; case "values": return data.attributes.every(attribute => attribute.values.length > 0); case "prices": @@ -77,6 +75,7 @@ export interface ProductVariantCreateDialogProps ProductVariantCreateContentProps, "data" | "dispatchFormDataAction" | "step" > { + defaultPrice: string; open: boolean; onClose: () => void; onSubmit: (data: ProductVariantBulkCreateInput[]) => void; @@ -85,17 +84,19 @@ export interface ProductVariantCreateDialogProps const ProductVariantCreateDialog: React.FC< ProductVariantCreateDialogProps > = props => { - const { open, onClose, onSubmit, ...contentProps } = props; + const { + attributes, + defaultPrice, + open, + onClose, + onSubmit, + ...contentProps + } = props; const classes = useStyles(props); - const [step, setStep] = React.useState( - "attributes" - ); + const [step, setStep] = React.useState("values"); function handleNextStep() { switch (step) { - case "attributes": - setStep("values"); - break; case "values": setStep("prices"); break; @@ -107,9 +108,6 @@ const ProductVariantCreateDialog: React.FC< function handlePrevStep() { switch (step) { - case "values": - setStep("attributes"); - break; case "prices": setStep("values"); break; @@ -121,7 +119,16 @@ const ProductVariantCreateDialog: React.FC< const [data, dispatchFormDataAction] = React.useReducer( reduceProductVariantCreateFormData, - initialForm + createInitialForm(attributes, defaultPrice) + ); + + React.useEffect( + () => + dispatchFormDataAction({ + data: createInitialForm(attributes, defaultPrice), + type: "reload" + }), + [attributes.length] ); return ( @@ -135,6 +142,7 @@ const ProductVariantCreateDialog: React.FC< - {step !== "attributes" && ( + {step !== "values" && ( + + 100g + + + + + +
+
+ Coffee Genre +
+
+
+ + +
+
+ Collar +
+
+
+ + + +
+
@@ -84070,284 +84296,6 @@ exports[`Storyshots Views / Products / Create multiple variants prices and SKU 1 `; -exports[`Storyshots Views / Products / Create multiple variants select values 1`] = ` -
-
-
-
-
-
- - Select Values - -
-
- - Prices and SKU - -
-
- - Summary - -
-
-
-
- Box Size -
-
-
- - - - -
-
- Coffee Genre -
-
-
- - -
-
- Collar -
-
-
- - - -
-
-
-
-
-
-`; - exports[`Storyshots Views / Products / Create product When loading 1`] = `
Date: Mon, 7 Oct 2019 12:02:09 +0200 Subject: [PATCH 21/32] Fix checkbox selection --- .../ProductVariantCreateValues.tsx | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx index b906904c3..3744bf90a 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx @@ -4,6 +4,7 @@ import makeStyles from "@material-ui/styles/makeStyles"; import React from "react"; import ControlledCheckbox from "@saleor/components/ControlledCheckbox"; +import Debounce from "@saleor/components/Debounce"; import Hr from "@saleor/components/Hr"; import Skeleton from "@saleor/components/Skeleton"; import { maybe } from "@saleor/misc"; @@ -46,19 +47,26 @@ const ProductVariantCreateValues: React.FC<
{attribute.values.map(value => ( - attribute.id === dataAttribute.id - ).values, - (a, b) => a === b + onValueClick(attribute.id, value.slug)} + time={100} + > + {change => ( + attribute.id === dataAttribute.id + ).values, + (a, b) => a === b + )} + name={`value:${value.slug}`} + label={value.name} + onChange={change} + key={value.slug} + /> )} - name={`value:${value.slug}`} - label={value.name} - onChange={() => onValueClick(attribute.id, value.slug)} - key={value.slug} - /> + ))}
From 8e9e412865535717b9fabff6075f7eaaa879fea9 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Mon, 7 Oct 2019 14:20:27 +0200 Subject: [PATCH 22/32] Fix ripple clipping --- .../ProductVariantCreateContent.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx index ef044632a..28ee51a60 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx @@ -1,6 +1,7 @@ +import { Theme } from "@material-ui/core/styles"; +import { makeStyles } from "@material-ui/styles"; import React from "react"; -import { makeStyles } from "@material-ui/styles"; import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails"; import { ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors } from "@saleor/products/types/ProductVariantBulkCreate"; import { isSelected } from "@saleor/utils/lists"; @@ -12,12 +13,16 @@ import ProductVariantCreateValues from "./ProductVariantCreateValues"; import { ProductVariantCreateReducerAction } from "./reducer"; import { ProductVariantCreateStep } from "./types"; -const useStyles = makeStyles({ +const useStyles = makeStyles((theme: Theme) => ({ root: { maxHeight: 400, - overflowY: "scroll" + overflowY: "scroll", + paddingLeft: theme.spacing.unit * 3, + position: "relative", + right: theme.spacing.unit * 3, + width: `calc(100% + ${theme.spacing.unit * 3}px)` } -}); +})); export interface ProductVariantCreateContentProps { attributes: ProductDetails_product_productType_variantAttributes[]; From be674522e11910733a0e05df289676ea094838b8 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Mon, 7 Oct 2019 15:58:51 +0200 Subject: [PATCH 23/32] Shorten inputs --- .../ProductVariantCreateContent.tsx | 2 ++ .../ProductVariantCreatePrices.tsx | 25 ++++++++++++++--- .../__snapshots__/Stories.test.ts.snap | 27 ++++++++++++------- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx index 28ee51a60..01944bf5c 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx @@ -16,6 +16,7 @@ import { ProductVariantCreateStep } from "./types"; const useStyles = makeStyles((theme: Theme) => ({ root: { maxHeight: 400, + overflowX: "hidden", overflowY: "scroll", paddingLeft: theme.spacing.unit * 3, position: "relative", @@ -74,6 +75,7 @@ const ProductVariantCreateContent: React.FC< {step === "prices" && ( dispatchFormDataAction({ diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx index 480fbfa6b..d8d2fb0ea 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx @@ -20,6 +20,9 @@ const useStyles = makeStyles((theme: Theme) => ({ marginBottom: theme.spacing.unit, marginTop: theme.spacing.unit / 2 }, + hrAttribute: { + marginTop: theme.spacing.unit * 2 + }, label: { alignSelf: "center" }, @@ -31,6 +34,7 @@ const useStyles = makeStyles((theme: Theme) => ({ export type PriceOrStock = "price" | "stock"; export interface ProductVariantCreatePricesProps { attributes: ProductDetails_product_productType_variantAttributes[]; + currencySymbol: string; data: ProductVariantCreateFormData; onApplyPriceOrStockChange: (applyToAll: boolean, type: PriceOrStock) => void; onApplyToAllChange: (value: string, type: PriceOrStock) => void; @@ -47,6 +51,7 @@ const ProductVariantCreatePrices: React.FC< > = props => { const { attributes, + currencySymbol, data, onApplyPriceOrStockChange, onApplyToAllChange, @@ -108,6 +113,9 @@ const ProductVariantCreatePrices: React.FC< min: 0, type: "number" }} + InputProps={{ + endAdornment: currencySymbol + }} label={intl.formatMessage({ defaultMessage: "Price", id: "productVariantCreatePricesPriceInputLabel" @@ -128,7 +136,7 @@ const ProductVariantCreatePrices: React.FC< {!data.price.all && ( <> - +
+
{priceAttributeValues && priceAttributeValues.map((attributeValue, attributeValueIndex) => ( <> - +
{attributeValue.name}
@@ -166,6 +175,13 @@ const ProductVariantCreatePrices: React.FC< description: "variant price", id: "productVariantCreatePricesSetPricePlaceholder" })} + inputProps={{ + min: 0, + type: "number" + }} + InputProps={{ + endAdornment: currencySymbol + }} fullWidth value={data.price.values[attributeValueIndex].value} onChange={event => @@ -226,7 +242,7 @@ const ProductVariantCreatePrices: React.FC< {!data.stock.all && ( <> - +
+
{stockAttributeValues && stockAttributeValues.map((attributeValue, attributeValueIndex) => ( <> - +
{attributeValue.name}
diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index a4f3bf4d2..5cae43c9c 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -83850,7 +83850,7 @@ exports[`Storyshots Views / Products / Create multiple variants prices and SKU 1 Price
+
@@ -84167,7 +84173,7 @@ exports[`Storyshots Views / Products / Create multiple variants prices and SKU 1 class="FormSpacer-spacer-id" />
+
Date: Mon, 7 Oct 2019 16:06:14 +0200 Subject: [PATCH 24/32] Fix bug after taking step back --- .../ProductVariantCreatePrices.tsx | 14 +++++++++++--- .../ProductVariantCreateDialog/reducer.ts | 14 +++++++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx index d8d2fb0ea..74d692bfd 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx @@ -183,7 +183,11 @@ const ProductVariantCreatePrices: React.FC< endAdornment: currencySymbol }} fullWidth - value={data.price.values[attributeValueIndex].value} + value={ + data.price.values.find( + value => value.slug === attributeValue.slug + ).value + } onChange={event => onAttributeValueChange( attributeValue.slug, @@ -267,7 +271,7 @@ const ProductVariantCreatePrices: React.FC<
{stockAttributeValues && - stockAttributeValues.map((attributeValue, attributeValueIndex) => ( + stockAttributeValues.map(attributeValue => ( <> @@ -282,7 +286,11 @@ const ProductVariantCreatePrices: React.FC< id: "productVariantCreatePricesSetStockPlaceholder" })} fullWidth - value={data.stock.values[attributeValueIndex].value} + value={ + data.stock.values.find( + value => value.slug === attributeValue.slug + ).value + } onChange={event => onAttributeValueChange( attributeValue.slug, diff --git a/src/products/components/ProductVariantCreateDialog/reducer.ts b/src/products/components/ProductVariantCreateDialog/reducer.ts index aa5b09ef2..d8c05c2a2 100644 --- a/src/products/components/ProductVariantCreateDialog/reducer.ts +++ b/src/products/components/ProductVariantCreateDialog/reducer.ts @@ -55,7 +55,19 @@ function selectValue( return { ...prevState, - attributes: updatedAttributes + attributes: updatedAttributes, + price: { + ...prevState.price, + all: true, + attribute: undefined, + values: [] + }, + stock: { + ...prevState.stock, + all: true, + attribute: undefined, + values: [] + } }; } From a770c464c7f7d6a162eb77469beca2502de41af7 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Mon, 7 Oct 2019 16:54:05 +0200 Subject: [PATCH 25/32] Remove unused value --- .../ProductVariantCreateDialog/ProductVariantCreatePrices.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx index 74d692bfd..2880d8ad4 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreatePrices.tsx @@ -161,7 +161,7 @@ const ProductVariantCreatePrices: React.FC<
{priceAttributeValues && - priceAttributeValues.map((attributeValue, attributeValueIndex) => ( + priceAttributeValues.map(attributeValue => ( <> From 03374fb00bf3a55477539bff3c4ae997b691cd51 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 8 Oct 2019 12:56:24 +0200 Subject: [PATCH 26/32] Minor fixes --- .../ProductVariantCreateContent.tsx | 6 ++- .../ProductVariantCreateDialog.tsx | 10 ++++- .../ProductVariantCreateTabs.tsx | 44 +++++++++++-------- .../ProductVariantCreateValues.tsx | 2 +- .../__snapshots__/Stories.test.ts.snap | 18 ++++---- src/theme.ts | 2 +- 6 files changed, 50 insertions(+), 32 deletions(-) diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx index 01944bf5c..8e5462237 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx @@ -32,6 +32,7 @@ export interface ProductVariantCreateContentProps { dispatchFormDataAction: React.Dispatch; errors: ProductVariantBulkCreate_productVariantBulkCreate_bulkProductErrors[]; step: ProductVariantCreateStep; + onStepClick: (step: ProductVariantCreateStep) => void; } const ProductVariantCreateContent: React.FC< @@ -43,7 +44,8 @@ const ProductVariantCreateContent: React.FC< data, dispatchFormDataAction, errors, - step + step, + onStepClick } = props; const classes = useStyles(props); @@ -57,7 +59,7 @@ const ProductVariantCreateContent: React.FC< return (
- +
{step === "values" && ( ({ overflowX: "visible", overflowY: "hidden", width: 800 + }, + spacer: { + flex: 1 } })); @@ -146,19 +149,24 @@ const ProductVariantCreateDialog: React.FC< data={data} dispatchFormDataAction={dispatchFormDataAction} step={step} + onStepClick={step => setStep(step)} /> +
{step !== "values" && ( )} {step !== "summary" ? ( diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.tsx index 5dca6bf57..3d4667f23 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateTabs.tsx @@ -51,13 +51,15 @@ const useStyles = makeStyles( }, tab: { flex: 1, - paddingBottom: theme.spacing.unit + paddingBottom: theme.spacing.unit, + userSelect: "none" }, tabActive: { fontWeight: 600 }, - tabUnderline: { - borderBottom: `3px solid ${theme.palette.primary.main}` + tabVisited: { + borderBottom: `3px solid ${theme.palette.primary.main}`, + cursor: "pointer" } }), { @@ -67,32 +69,38 @@ const useStyles = makeStyles( export interface ProductVariantCreateTabsProps { step: ProductVariantCreateStep; + onStepClick: (step: ProductVariantCreateStep) => void; } const ProductVariantCreateTabs: React.FC< ProductVariantCreateTabsProps > = props => { - const { step: currentStep } = props; + const { step: currentStep, onStepClick } = props; const classes = useStyles(props); const intl = useIntl(); const steps = getSteps(intl); return (
- {steps.map((step, stepIndex) => ( -
step.value === currentStep) >= stepIndex - })} - key={step.value} - > - - {step.label} - -
- ))} + {steps.map((step, stepIndex) => { + const visitedStep = + steps.findIndex(step => step.value === currentStep) >= stepIndex; + + return ( +
onStepClick(step.value) : undefined} + key={step.value} + > + + {step.label} + +
+ ); + })}
); }; diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx index 3744bf90a..dd6fe6dc3 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateValues.tsx @@ -50,6 +50,7 @@ const ProductVariantCreateValues: React.FC< onValueClick(attribute.id, value.slug)} time={100} + key={value.slug} > {change => ( )} diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index 5cae43c9c..c97b20359 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -82067,7 +82067,7 @@ exports[`Storyshots Views / Products / Create multiple variants / summary defaul class="ProductVariantCreateTabs-root-id" >
}, flat: { "& span": { - color: colors.primary + color: colors.font.gray } }, flatPrimary: { From 485a471fe3850e671eb0da44923f86daa0ba3ede Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 8 Oct 2019 13:34:47 +0200 Subject: [PATCH 27/32] Update messages --- locale/messages.pot | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/locale/messages.pot b/locale/messages.pot index 78fc1bb3a..1dcc13d48 100644 --- a/locale/messages.pot +++ b/locale/messages.pot @@ -1,6 +1,6 @@ msgid "" msgstr "" -"POT-Creation-Date: 2019-10-09T10:24:36.690Z\n" +"POT-Creation-Date: 2019-10-09T10:25:56.800Z\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "MIME-Version: 1.0\n" @@ -1487,10 +1487,6 @@ msgstr "" #. [src.back] - button #. defaultMessage is: #. Back -#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.json -#. [src.products.components.ProductVariantCreateDialog.1347475195] - button -#. defaultMessage is: -#. Back msgctxt "button" msgid "Back" msgstr "" @@ -5743,6 +5739,14 @@ msgctxt "order payment" msgid "Preauthorized amount" msgstr "" +#: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.json +#. [src.products.components.ProductVariantCreateDialog.904693740] - previous step, button +#. defaultMessage is: +#. Previous +msgctxt "previous step, button" +msgid "Previous" +msgstr "" + #: build/locale/src/categories/components/CategoryProductList/CategoryProductList.json #. [src.categories.components.CategoryProductList.1134347598] - product price #. defaultMessage is: From 0318074806f8413c6b41e0369ecdba17db728ad8 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 8 Oct 2019 14:08:41 +0200 Subject: [PATCH 28/32] Fix types --- .../ProductVariantCreateDialog/ProductVariantCreateDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx index f46e984c2..dcb42c9db 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx @@ -76,7 +76,7 @@ function canHitNext( export interface ProductVariantCreateDialogProps extends Omit< ProductVariantCreateContentProps, - "data" | "dispatchFormDataAction" | "step" + "data" | "dispatchFormDataAction" | "step" | "onStepClick" > { defaultPrice: string; open: boolean; From 12aed6457ed639fb4dfe1953a99c308f971ebb60 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 8 Oct 2019 14:38:15 +0200 Subject: [PATCH 29/32] Fix story --- .../ProductVariantCreateDialog/ProductVariantCreate.stories.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreate.stories.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreate.stories.tsx index 31722b97b..2caf5fba2 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreate.stories.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreate.stories.tsx @@ -69,6 +69,7 @@ const props: ProductVariantCreateContentProps = { }, dispatchFormDataAction: () => undefined, errors: [], + onStepClick: () => undefined, step: "values" }; From b75ceed7cce8b706afbfd1c306dde2698d7dcae7 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Wed, 9 Oct 2019 12:45:27 +0200 Subject: [PATCH 30/32] Add attribute values to matrix --- .../ProductVariantCreateDialog/reducer.ts | 39 ++++++++++++++----- .../__snapshots__/Stories.test.ts.snap | 8 ++-- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/products/components/ProductVariantCreateDialog/reducer.ts b/src/products/components/ProductVariantCreateDialog/reducer.ts index d8c05c2a2..ee8480ef0 100644 --- a/src/products/components/ProductVariantCreateDialog/reducer.ts +++ b/src/products/components/ProductVariantCreateDialog/reducer.ts @@ -1,3 +1,4 @@ +//#region import { add, remove, @@ -35,16 +36,16 @@ export interface ProductVariantCreateReducerAction { valueId?: string; variantIndex?: number; } - +//#endregion function selectValue( prevState: ProductVariantCreateFormData, attributeId: string, - valueId: string + valueSlug: string ): ProductVariantCreateFormData { const attribute = prevState.attributes.find( attribute => attribute.id === attributeId ); - const values = toggle(valueId, attribute.values, (a, b) => a === b); + const values = toggle(valueSlug, attribute.values, (a, b) => a === b); const updatedAttributes = add( { id: attributeId, @@ -53,20 +54,40 @@ function selectValue( remove(attribute, prevState.attributes, (a, b) => a.id === b.id) ); + const priceValues = + prevState.price.attribute === attributeId + ? toggle( + { + slug: valueSlug, + value: "" + }, + prevState.price.values, + (a, b) => a.slug === b.slug + ) + : prevState.price.values; + + const stockValues = + prevState.stock.attribute === attributeId + ? toggle( + { + slug: valueSlug, + value: "" + }, + prevState.stock.values, + (a, b) => a.slug === b.slug + ) + : prevState.stock.values; + return { ...prevState, attributes: updatedAttributes, price: { ...prevState.price, - all: true, - attribute: undefined, - values: [] + values: priceValues }, stock: { ...prevState.stock, - all: true, - attribute: undefined, - values: [] + values: stockValues } }; } diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index c97b20359..f1fffe1cf 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -82056,7 +82056,7 @@ exports[`Storyshots Views / Products / Create multiple variants / summary defaul style="padding:24px" >
Date: Wed, 9 Oct 2019 12:52:51 +0200 Subject: [PATCH 31/32] Reset form state after closing it --- .../ProductVariantCreateDialog.tsx | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx index dcb42c9db..c898e9157 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.tsx @@ -8,6 +8,8 @@ import { makeStyles } from "@material-ui/styles"; import React from "react"; import { FormattedMessage } from "react-intl"; +import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors"; +import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen"; import { ProductVariantBulkCreateInput } from "../../../types/globalTypes"; import { createInitialForm, ProductVariantCreateFormData } from "./form"; import ProductVariantCreateContent, { @@ -90,6 +92,7 @@ const ProductVariantCreateDialog: React.FC< const { attributes, defaultPrice, + errors: apiErrors, open, onClose, onSubmit, @@ -125,14 +128,22 @@ const ProductVariantCreateDialog: React.FC< createInitialForm(attributes, defaultPrice) ); - React.useEffect( - () => - dispatchFormDataAction({ - data: createInitialForm(attributes, defaultPrice), - type: "reload" - }), - [attributes.length] - ); + const reloadForm = () => + dispatchFormDataAction({ + data: createInitialForm(attributes, defaultPrice), + type: "reload" + }); + + React.useEffect(reloadForm, [attributes.length]); + + useModalDialogOpen(open, { + onClose: () => { + reloadForm(); + setStep("values"); + } + }); + + const errors = useModalDialogErrors(apiErrors, open); return ( @@ -148,6 +159,7 @@ const ProductVariantCreateDialog: React.FC< attributes={attributes} data={data} dispatchFormDataAction={dispatchFormDataAction} + errors={errors} step={step} onStepClick={step => setStep(step)} /> From da58dabeb69980da86a2997ebe42cbafbd3783d6 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Wed, 9 Oct 2019 13:31:45 +0200 Subject: [PATCH 32/32] Add right padding --- .../ProductVariantCreateDialog/ProductVariantCreateContent.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx index 8e5462237..cbc32664f 100644 --- a/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx +++ b/src/products/components/ProductVariantCreateDialog/ProductVariantCreateContent.tsx @@ -19,6 +19,7 @@ const useStyles = makeStyles((theme: Theme) => ({ overflowX: "hidden", overflowY: "scroll", paddingLeft: theme.spacing.unit * 3, + paddingRight: theme.spacing.unit * 2, position: "relative", right: theme.spacing.unit * 3, width: `calc(100% + ${theme.spacing.unit * 3}px)`