Add variant creator wip

This commit is contained in:
dominik-zeglen 2019-09-19 12:13:04 +02:00
parent 2637ec3fdf
commit 5a40f619e1
12 changed files with 1263 additions and 1 deletions

View file

@ -1,3 +1,4 @@
import { ProductDetails_product_productType_variantAttributes } from "@saleor/products/types/ProductDetails";
import { import {
AttributeInputTypeEnum, AttributeInputTypeEnum,
AttributeValueType AttributeValueType
@ -35,7 +36,10 @@ export const attribute: AttributeDetailsFragment = {
visibleInStorefront: true visibleInStorefront: true
}; };
export const attributes: AttributeList_attributes_edges_node[] = [ export const attributes: Array<
AttributeList_attributes_edges_node &
ProductDetails_product_productType_variantAttributes
> = [
{ {
node: { node: {
__typename: "Attribute" as "Attribute", __typename: "Attribute" as "Attribute",

View file

@ -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 => (
<Card
style={{
margin: "auto",
overflow: "visible",
width: 600
}}
>
<CardContent>{storyFn()}</CardContent>
</Card>
))
.addDecorator(Decorator)
.add("choose attributes", () => <ProductVariantCreateContent {...props} />)
.add("select values", () => (
<ProductVariantCreateContent {...props} step="values" />
))
.add("prices and SKU", () => (
<ProductVariantCreateContent {...props} step="prices" />
))
.add("summary", () => (
<ProductVariantCreateContent {...props} step="summary" />
));
storiesOf("Views / Products / Create multiple variants", module)
.addDecorator(Decorator)
.add("interactive", () => (
<ProductVariantCreateDialog
{...props}
open={true}
onClose={() => undefined}
onSubmit={() => undefined}
/>
));

View file

@ -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 (
<Table key="table">
<TableBody>
{renderCollection(
attributes,
attribute => {
if (!attribute) {
return null;
}
const isChecked = !!data.attributes.find(
selectedAttribute => selectedAttribute === attribute.id
);
return (
<TableRow key={maybe(() => attribute.id)}>
<TableCell padding="checkbox" className={classes.checkboxCell}>
<Checkbox
checked={isChecked}
disableClickPropagation={true}
onChange={() => onAttributeClick(attribute.id)}
/>
</TableCell>
<TableCell className={classes.wideCell}>
{attribute.name}
</TableCell>
</TableRow>
);
},
() => (
<TableRow>
<TableCell colSpan={2}>
<FormattedMessage defaultMessage="This product type has no variant attributes" />
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
);
};
ProductVariantCreateAttributes.displayName = "ProductVariantCreateAttributes";
export default ProductVariantCreateAttributes;

View file

@ -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<ProductVariantCreateReducerAction>;
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 (
<div>
<ProductVariantCreateTabs step={step} />
<div className={classes.root}>
{step === "attributes" && (
<ProductVariantCreateAttributes
attributes={attributes}
data={data}
onAttributeClick={id =>
dispatchFormDataAction({
id,
type: "selectAttribute"
})
}
/>
)}
{step === "values" && (
<ProductVariantCreateValues
attributes={selectedAttributes}
data={data}
onValueClick={id =>
dispatchFormDataAction({
id,
type: "selectValue"
})
}
/>
)}
{step === "prices" && (
<ProductVariantCreatePrices
attributes={selectedAttributes}
data={data}
onApplyPriceOrStockChange={(all, type) =>
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" && (
<ProductVariantCreateSummary
attributes={selectedAttributes}
currencySymbol={currencySymbol}
data={data}
/>
)}
</div>
</div>
);
};
ProductVariantCreateContent.displayName = "ProductVariantCreateContent";
export default ProductVariantCreateContent;

View file

@ -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<ProductVariantCreateStep>(
"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 (
<Dialog open={open} maxWidth="md">
<DialogTitle>
<FormattedMessage
defaultMessage="Assign Attribute"
description="dialog header"
/>
</DialogTitle>
<DialogContent className={classes.content}>
<ProductVariantCreateContent
{...contentProps}
data={data}
dispatchFormDataAction={dispatchFormDataAction}
step={step}
/>
</DialogContent>
<DialogActions>
<Button className={classes.button} onClick={onClose}>
<FormattedMessage defaultMessage="Cancel" description="button" />
</Button>
{step !== "attributes" && (
<Button
className={classes.button}
color="primary"
onClick={handlePrevStep}
>
<FormattedMessage defaultMessage="Back" description="button" />
</Button>
)}
{step !== "summary" ? (
<Button
className={classes.button}
color="primary"
variant="contained"
onClick={handleNextStep}
>
<FormattedMessage defaultMessage="Next" description="button" />
</Button>
) : (
<Button className={classes.button} variant="contained">
<FormattedMessage
defaultMessage="Create"
description="create multiple variants, button"
/>
</Button>
)}
</DialogActions>
</Dialog>
);
};
ProductVariantCreateDialog.displayName = "ProductVariantCreateDialog";
export default ProductVariantCreateDialog;

View file

@ -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 (
<>
<Typography color="textSecondary" variant="headline">
<FormattedMessage
defaultMessage="Price"
description="variant price, header"
/>
</Typography>
<Hr className={classes.hr} />
<RadioGroup value={data.price.all ? "applyToAll" : "applyToAttribute"}>
<FormControlLabel
value="applyToAll"
control={<Radio color="primary" />}
label={intl.formatMessage({
defaultMessage: "Apply single price to all SKUs"
})}
onChange={() => onApplyPriceOrStockChange(true, "price")}
/>
<FormSpacer />
<TextField
className={classes.shortInput}
inputProps={{
min: 0,
type: "number"
}}
label={intl.formatMessage({
defaultMessage: "Price",
id: "productVariantCreatePricesPriceInputLabel"
})}
value={data.price.value}
onChange={event => onApplyToAllChange(event.target.value, "price")}
/>
<FormSpacer />
<FormControlLabel
value="applyToAttribute"
control={<Radio color="primary" />}
label={intl.formatMessage({
defaultMessage: "Apply unique prices by attribute to each SKU"
})}
onChange={() => onApplyPriceOrStockChange(false, "price")}
/>
{!data.price.all && (
<>
<FormSpacer />
<Grid variant="inverted">
<div className={classes.label}>
<Typography>
<FormattedMessage
defaultMessage="Choose attribute"
description="variant attribute"
/>
</Typography>
</div>
<div>
<SingleSelectField
choices={attributeChoices}
label={intl.formatMessage({
defaultMessage: "Attribute",
description: "variant attribute"
})}
value={data.price.attribute}
onChange={event =>
onAttributeSelect(event.target.value, "price")
}
/>
</div>
</Grid>
{priceAttributeValues &&
priceAttributeValues.map((attribute, attributeIndex) => (
<>
<FormSpacer />
<Grid variant="inverted">
<div className={classes.label}>
<Typography>{attribute.name}</Typography>
</div>
<div>
<TextField
label={intl.formatMessage({
defaultMessage: "Price",
description: "variant price",
id: "productVariantCreatePricesSetPricePlaceholder"
})}
fullWidth
value={data.price.values[attributeIndex]}
/>
</div>
</Grid>
</>
))}
</>
)}
</RadioGroup>
<FormSpacer />
<Typography color="textSecondary" variant="headline">
<FormattedMessage
defaultMessage="Stock"
description="variant stock, header"
/>
</Typography>
<Hr className={classes.hr} />
<RadioGroup value={data.stock.all ? "applyToAll" : "applyToAttribute"}>
<FormControlLabel
value="applyToAll"
control={<Radio color="primary" />}
label={intl.formatMessage({
defaultMessage: "Apply single stock to all SKUs"
})}
onChange={() => onApplyPriceOrStockChange(true, "stock")}
/>
<FormSpacer />
<TextField
className={classes.shortInput}
inputProps={{
min: 0,
type: "number"
}}
label={intl.formatMessage({
defaultMessage: "Stock",
id: "productVariantCreatePricesStockInputLabel"
})}
value={data.stock.value}
onChange={event => onApplyToAllChange(event.target.value, "stock")}
/>
<FormSpacer />
<FormControlLabel
value="applyToAttribute"
control={<Radio color="primary" />}
label={intl.formatMessage({
defaultMessage: "Apply unique stock by attribute to each SKU"
})}
onChange={() => onApplyPriceOrStockChange(false, "stock")}
/>
{!data.stock.all && (
<>
<FormSpacer />
<Grid variant="inverted">
<div className={classes.label}>
<Typography>
<FormattedMessage
defaultMessage="Choose attribute"
description="variant attribute"
/>
</Typography>
</div>
<div>
<SingleSelectField
choices={attributeChoices}
label={intl.formatMessage({
defaultMessage: "Attribute",
description: "variant attribute"
})}
value={data.stock.attribute}
onChange={event =>
onAttributeSelect(event.target.value, "stock")
}
/>
</div>
</Grid>
{stockAttributeValues &&
stockAttributeValues.map((attribute, attributeIndex) => (
<>
<FormSpacer />
<Grid variant="inverted">
<div className={classes.label}>
<Typography>{attribute.name}</Typography>
</div>
<div>
<TextField
label={intl.formatMessage({
defaultMessage: "Stock",
description: "variant stock",
id: "productVariantCreatePricesSetStockPlaceholder"
})}
fullWidth
value={data.stock.values[attributeIndex]}
/>
</div>
</Grid>
</>
))}
</>
)}
</RadioGroup>
</>
);
};
ProductVariantCreatePrices.displayName = "ProductVariantCreatePrices";
export default ProductVariantCreatePrices;

View file

@ -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 (
<>
<Typography color="textSecondary" variant="headline">
<FormattedMessage
defaultMessage="You will create variants below"
description="header"
/>
</Typography>
<Hr className={classes.hr} />
<Table>
<TableHead>
<TableRow>
<TableCell className={classNames(classes.col, classes.colName)}>
<FormattedMessage
defaultMessage="Variant"
description="variant name"
/>
</TableCell>
<TableCell className={classNames(classes.col, classes.colStock)}>
<FormattedMessage
defaultMessage="Inventory"
description="variant stock amount"
/>
</TableCell>
<TableCell className={classNames(classes.col, classes.colPrice)}>
<FormattedMessage
defaultMessage="Price"
description="variant price"
/>
</TableCell>
<TableCell className={classNames(classes.col, classes.colSku)}>
<FormattedMessage defaultMessage="SKU" />
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data.variants.map(variant => (
<TableRow
key={variant.attributes.map(attribute => attribute.id).join(":")}
>
<TableCell className={classNames(classes.col, classes.colName)}>
{getVariantName(variant, attributes).join(" ")}
</TableCell>
<TableCell className={classNames(classes.col, classes.colStock)}>
<TextField
className={classes.input}
inputProps={{
min: 0,
type: "number"
}}
fullWidth
value={variant.quantity}
/>
</TableCell>
<TableCell className={classNames(classes.col, classes.colPrice)}>
<TextField
InputProps={{
endAdornment: currencySymbol
}}
className={classes.input}
inputProps={{
min: 0,
type: "number"
}}
fullWidth
value={variant.priceOverride}
/>
</TableCell>
<TableCell className={classNames(classes.col, classes.colSku)}>
<TextField
className={classes.input}
fullWidth
value={variant.sku}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</>
);
};
ProductVariantCreateSummary.displayName = "ProductVariantCreateSummary";
export default ProductVariantCreateSummary;

View file

@ -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 (
<div className={classes.root}>
{steps.map((step, stepIndex) => (
<div
className={classNames(classes.tab, {
[classes.tabActive]: step.value === currentStep,
[classes.tabUnderline]:
steps.findIndex(step => step.value === currentStep) >= stepIndex
})}
>
<Typography className={classes.label} variant="caption">
{step.label}
</Typography>
</div>
))}
</div>
);
};
ProductVariantCreateTabs.displayName = "ProductVariantCreateTabs";
export default ProductVariantCreateTabs;

View file

@ -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 => (
<>
<Typography color="textSecondary" variant="headline">
{maybe<React.ReactNode>(() => attribute.name, <Skeleton />)}
</Typography>
<Hr className={classes.hr} />
<div className={classes.valueContainer}>
{attribute.values.map(value => (
<ControlledCheckbox
checked={isSelected(value.id, data.values, (a, b) => a === b)}
name={`value:${value.id}`}
label={value.name}
onChange={() => onValueClick(value.id)}
/>
))}
</div>
</>
))}
</>
);
};
ProductVariantCreateValues.displayName = "ProductVariantCreateValues";
export default ProductVariantCreateValues;

View file

@ -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: []
};

View file

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

View file

@ -0,0 +1,5 @@
export type ProductVariantCreateStep =
| "attributes"
| "values"
| "prices"
| "summary";