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