Add variant creator wip
This commit is contained in:
parent
2637ec3fdf
commit
5a40f619e1
12 changed files with 1263 additions and 1 deletions
|
@ -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",
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
));
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
33
src/products/components/ProductVariantCreateDialog/form.ts
Normal file
33
src/products/components/ProductVariantCreateDialog/form.ts
Normal 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: []
|
||||
};
|
192
src/products/components/ProductVariantCreateDialog/reducer.ts
Normal file
192
src/products/components/ProductVariantCreateDialog/reducer.ts
Normal 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;
|
|
@ -0,0 +1,5 @@
|
|||
export type ProductVariantCreateStep =
|
||||
| "attributes"
|
||||
| "values"
|
||||
| "prices"
|
||||
| "summary";
|
Loading…
Reference in a new issue