Refactor product section translations (#125)

This commit is contained in:
Dominik Żegleń 2019-08-26 19:53:22 +02:00 committed by dominik-zeglen
parent c214cc298d
commit 63e4f9bd34
31 changed files with 2120 additions and 896 deletions

File diff suppressed because it is too large Load diff

View file

@ -133,6 +133,10 @@ export const sectionNames = defineMessages({
defaultMessage: "Product Types", defaultMessage: "Product Types",
description: "product types section name" description: "product types section name"
}, },
products: {
defaultMessage: "Products",
description: "products section name"
},
sales: { sales: {
defaultMessage: "Sales", defaultMessage: "Sales",
description: "sales section name" description: "sales section name"

View file

@ -7,6 +7,7 @@ import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
import makeStyles from "@material-ui/styles/makeStyles"; import makeStyles from "@material-ui/styles/makeStyles";
import classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
@ -18,7 +19,6 @@ import SingleAutocompleteSelectField, {
SingleAutocompleteChoiceType SingleAutocompleteChoiceType
} from "@saleor/components/SingleAutocompleteSelectField"; } from "@saleor/components/SingleAutocompleteSelectField";
import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset"; import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset";
import i18n from "@saleor/i18n";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { ProductDetails_product_attributes_attribute_values } from "@saleor/products/types/ProductDetails"; import { ProductDetails_product_attributes_attribute_values } from "@saleor/products/types/ProductDetails";
import { AttributeInputTypeEnum } from "@saleor/types/globalTypes"; import { AttributeInputTypeEnum } from "@saleor/types/globalTypes";
@ -127,21 +127,30 @@ const ProductAttributes: React.FC<ProductAttributesProps> = ({
onChange, onChange,
onMultiChange onMultiChange
}) => { }) => {
const intl = useIntl();
const classes = useStyles({}); const classes = useStyles({});
const [expanded, setExpansionStatus] = React.useState(true); const [expanded, setExpansionStatus] = React.useState(true);
const toggleExpansion = () => setExpansionStatus(!expanded); const toggleExpansion = () => setExpansionStatus(!expanded);
return ( return (
<Card className={classes.card}> <Card className={classes.card}>
<CardTitle title={i18n.t("Attributes")} /> <CardTitle
title={intl.formatMessage({
defaultMessage: "Attributes",
description: "product attributes, section header"
})}
/>
<CardContent className={classes.cardContent}> <CardContent className={classes.cardContent}>
<div className={classes.expansionBar}> <div className={classes.expansionBar}>
<div className={classes.expansionBarLabelContainer}> <div className={classes.expansionBarLabelContainer}>
<Typography className={classes.expansionBarLabel} variant="caption"> <Typography className={classes.expansionBarLabel} variant="caption">
{i18n.t("{{ number }} Attributes", { <FormattedMessage
context: "number of attribute", defaultMessage="{number} Attributes"
number: attributes.length description="number of product attributes"
})} values={{
number: attributes.length
}}
/>
</Typography> </Typography>
</div> </div>
<IconButton <IconButton
@ -184,7 +193,10 @@ const ProductAttributes: React.FC<ProductAttributesProps> = ({
)} )}
emptyOption emptyOption
name={`attribute:${attribute.label}`} name={`attribute:${attribute.label}`}
label={i18n.t("Value")} label={intl.formatMessage({
defaultMessage: "Value",
description: "attribute value"
})}
value={attribute.value[0]} value={attribute.value[0]}
onChange={event => onChange={event =>
onChange(attribute.id, event.target.value) onChange(attribute.id, event.target.value)
@ -195,7 +207,10 @@ const ProductAttributes: React.FC<ProductAttributesProps> = ({
<MultiAutocompleteSelectField <MultiAutocompleteSelectField
choices={getMultiChoices(attribute.data.values)} choices={getMultiChoices(attribute.data.values)}
displayValues={getMultiDisplayValue(attribute)} displayValues={getMultiDisplayValue(attribute)}
label={i18n.t("Values")} label={intl.formatMessage({
defaultMessage: "Values",
description: "attribute values"
})}
name={`attribute:${attribute.label}`} name={`attribute:${attribute.label}`}
value={attribute.value} value={attribute.value}
onChange={event => onChange={event =>

View file

@ -1,12 +1,12 @@
import Card from "@material-ui/core/Card"; import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent"; import CardContent from "@material-ui/core/CardContent";
import React from "react"; import React from "react";
import { useIntl } from "react-intl";
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";
import MultiSelectField from "@saleor/components/MultiSelectField"; import MultiSelectField from "@saleor/components/MultiSelectField";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SingleSelectField from "@saleor/components/SingleSelectField"; import SingleSelectField from "@saleor/components/SingleSelectField";
import i18n from "../../../i18n";
interface ProductCategoryAndCollectionsFormProps { interface ProductCategoryAndCollectionsFormProps {
categories?: Array<{ value: string; label: string }>; categories?: Array<{ value: string; label: string }>;
@ -26,34 +26,47 @@ const ProductCategoryAndCollectionsForm = ({
category, category,
loading, loading,
onChange onChange
}: ProductCategoryAndCollectionsFormProps) => ( }: ProductCategoryAndCollectionsFormProps) => {
<Card> const intl = useIntl();
<PageHeader title={i18n.t("Organisation")} />
<CardContent> return (
<SingleSelectField <Card>
disabled={loading} <PageHeader
error={!!errors.category} title={intl.formatMessage({
hint={errors.category} defaultMessage: "Organization",
label={i18n.t("Category")} description: "product organization, header"
choices={loading ? [] : categories} })}
name="category"
value={category}
onChange={onChange}
/> />
<FormSpacer /> <CardContent>
<MultiSelectField <SingleSelectField
disabled={loading} disabled={loading}
error={!!errors.collections} error={!!errors.category}
hint={errors.collections} hint={errors.category}
label={i18n.t("Collections")} label={intl.formatMessage({
choices={loading ? [] : collections} defaultMessage: "Category"
name="collections" })}
value={productCollections} choices={loading ? [] : categories}
onChange={onChange} name="category"
/> value={category}
</CardContent> onChange={onChange}
</Card> />
); <FormSpacer />
<MultiSelectField
disabled={loading}
error={!!errors.collections}
hint={errors.collections}
label={intl.formatMessage({
defaultMessage: "Collections"
})}
choices={loading ? [] : collections}
name="collections"
value={productCollections}
onChange={onChange}
/>
</CardContent>
</Card>
);
};
ProductCategoryAndCollectionsForm.displayName = ProductCategoryAndCollectionsForm.displayName =
"ProductCategoryAndCollectionsForm"; "ProductCategoryAndCollectionsForm";
export default ProductCategoryAndCollectionsForm; export default ProductCategoryAndCollectionsForm;

View file

@ -1,5 +1,6 @@
import { ContentState, convertToRaw, RawDraftContentState } from "draft-js"; import { ContentState, convertToRaw, RawDraftContentState } from "draft-js";
import React from "react"; import React from "react";
import { useIntl } from "react-intl";
import AppHeader from "@saleor/components/AppHeader"; import AppHeader from "@saleor/components/AppHeader";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
@ -16,6 +17,7 @@ import { SearchCategories_categories_edges_node } from "@saleor/containers/Searc
import { SearchCollections_collections_edges_node } from "@saleor/containers/SearchCollections/types/SearchCollections"; import { SearchCollections_collections_edges_node } from "@saleor/containers/SearchCollections/types/SearchCollections";
import useFormset from "@saleor/hooks/useFormset"; import useFormset from "@saleor/hooks/useFormset";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { sectionNames } from "@saleor/intl";
import { import {
getChoices, getChoices,
ProductAttributeValueChoices, ProductAttributeValueChoices,
@ -23,7 +25,6 @@ import {
} from "@saleor/products/utils/data"; } from "@saleor/products/utils/data";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import i18n from "../../../i18n";
import { UserError } from "../../../types"; import { UserError } from "../../../types";
import { ProductCreateData_productTypes_edges_node_productAttributes } from "../../types/ProductCreateData"; import { ProductCreateData_productTypes_edges_node_productAttributes } from "../../types/ProductCreateData";
import { import {
@ -96,6 +97,8 @@ export const ProductCreatePage: React.StatelessComponent<
onBack, onBack,
onSubmit onSubmit
}: ProductCreatePageProps) => { }: ProductCreatePageProps) => {
const intl = useIntl();
// Form values // Form values
const { const {
change: changeAttributeData, change: changeAttributeData,
@ -200,7 +203,9 @@ export const ProductCreatePage: React.StatelessComponent<
return ( return (
<Container> <Container>
<AppHeader onBack={onBack}>{i18n.t("Products")}</AppHeader> <AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.products)}
</AppHeader>
<PageHeader title={header} /> <PageHeader title={header} />
<Grid> <Grid>
<div> <div>
@ -239,9 +244,10 @@ export const ProductCreatePage: React.StatelessComponent<
</> </>
)} )}
<SeoForm <SeoForm
helperText={i18n.t( helperText={intl.formatMessage({
"Add search engine title and description to make this product easier to find" defaultMessage:
)} "Add search engine title and description to make this product easier to find"
})}
title={data.seoTitle} title={data.seoTitle}
titlePlaceholder={data.name} titlePlaceholder={data.name}
description={data.seoDescription} description={data.seoDescription}

View file

@ -9,11 +9,12 @@ import {
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import { RawDraftContentState } from "draft-js"; import { RawDraftContentState } from "draft-js";
import React from "react"; import React from "react";
import { useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";
import RichTextEditor from "@saleor/components/RichTextEditor"; import RichTextEditor from "@saleor/components/RichTextEditor";
import i18n from "../../../i18n"; import { commonMessages } from "@saleor/intl";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@ -48,36 +49,45 @@ export const ProductDetailsForm = withStyles(styles, {
errors, errors,
initialDescription, initialDescription,
onChange onChange
}: ProductDetailsFormProps) => ( }: ProductDetailsFormProps) => {
<Card> const intl = useIntl();
<CardTitle title={i18n.t("General information")} />
<CardContent> return (
<div className={classes.root}> <Card>
<TextField <CardTitle
error={!!errors.name} title={intl.formatMessage(commonMessages.generalInformations)}
helperText={errors.name} />
<CardContent>
<div className={classes.root}>
<TextField
error={!!errors.name}
helperText={errors.name}
disabled={disabled}
fullWidth
label={intl.formatMessage({
defaultMessage: "Name",
description: "product name"
})}
name="name"
rows={5}
value={data.name}
onChange={onChange}
/>
</div>
<FormSpacer />
<RichTextEditor
disabled={disabled} disabled={disabled}
fullWidth error={!!errors.descriptionJson}
label={i18n.t("Name")} helperText={errors.descriptionJson}
name="name" initial={initialDescription}
rows={5} label={intl.formatMessage(commonMessages.description)}
value={data.name} name="description"
onChange={onChange} onChange={onChange}
/> />
</div> </CardContent>
<FormSpacer /> </Card>
<RichTextEditor );
disabled={disabled} }
error={!!errors.descriptionJson}
helperText={errors.descriptionJson}
initial={initialDescription}
label={i18n.t("Description")}
name="description"
onChange={onChange}
/>
</CardContent>
</Card>
)
); );
ProductDetailsForm.displayName = "ProductDetailsForm"; ProductDetailsForm.displayName = "ProductDetailsForm";
export default ProductDetailsForm; export default ProductDetailsForm;

View file

@ -8,10 +8,10 @@ import {
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import { useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import i18n from "../../../i18n";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@ -64,31 +64,41 @@ const ProductImageNavigation = withStyles(styles, {
highlighted, highlighted,
images, images,
onRowClick onRowClick
}: ProductImageNavigationProps) => ( }: ProductImageNavigationProps) => {
<Card className={classes.card}> const intl = useIntl();
<CardTitle title={i18n.t("All photos")} />
<CardContent> return (
{images === undefined ? ( <Card className={classes.card}>
<Skeleton /> <CardTitle
) : ( title={intl.formatMessage({
<div className={classes.root}> defaultMessage: "All Photos",
{images.map(image => ( description: "section header"
<div })}
className={classNames({ />
[classes.imageContainer]: true, <CardContent>
[classes.highlightedImageContainer]: image.id === highlighted {images === undefined ? (
})} <Skeleton />
onClick={onRowClick(image.id)} ) : (
key={image.id} <div className={classes.root}>
> {images.map(image => (
<img className={classes.image} src={image.url} /> <div
</div> className={classNames({
))} [classes.imageContainer]: true,
</div> [classes.highlightedImageContainer]:
)} image.id === highlighted
</CardContent> })}
</Card> onClick={onRowClick(image.id)}
) key={image.id}
>
<img className={classes.image} src={image.url} />
</div>
))}
</div>
)}
</CardContent>
</Card>
);
}
); );
ProductImageNavigation.displayName = "ProductImageNavigation"; ProductImageNavigation.displayName = "ProductImageNavigation";
export default ProductImageNavigation; export default ProductImageNavigation;

View file

@ -8,6 +8,7 @@ import {
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import React from "react"; import React from "react";
import { useIntl } from "react-intl";
import AppHeader from "@saleor/components/AppHeader"; import AppHeader from "@saleor/components/AppHeader";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
@ -18,7 +19,7 @@ import Grid from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import i18n from "../../../i18n"; import { commonMessages } from "@saleor/intl";
import ProductImageNavigation from "../ProductImageNavigation"; import ProductImageNavigation from "../ProductImageNavigation";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
@ -69,68 +70,90 @@ const ProductImagePage = withStyles(styles, { name: "ProductImagePage" })(
onDelete, onDelete,
onRowClick, onRowClick,
onSubmit onSubmit
}: ProductImagePageProps) => ( }: ProductImagePageProps) => {
<Form const intl = useIntl();
initial={{ description: image ? image.alt : "" }}
onSubmit={onSubmit} return (
confirmLeave <Form
> initial={{ description: image ? image.alt : "" }}
{({ change, data, hasChanged, submit }) => { onSubmit={onSubmit}
return ( confirmLeave
<Container> >
<AppHeader onBack={onBack}>{product}</AppHeader> {({ change, data, hasChanged, submit }) => {
<PageHeader title={i18n.t("Edit Photo")} /> return (
<Grid variant="inverted"> <Container>
<div> <AppHeader onBack={onBack}>{product}</AppHeader>
<ProductImageNavigation <PageHeader
disabled={disabled} title={intl.formatMessage({
images={images} defaultMessage: "Edit Photo",
highlighted={image ? image.id : undefined} description: "header"
onRowClick={onRowClick} })}
/> />
<Card> <Grid variant="inverted">
<CardTitle title={i18n.t("Photo Information")} /> <div>
<CardContent> <ProductImageNavigation
<TextField disabled={disabled}
name="description" images={images}
label={i18n.t("Description")} highlighted={image ? image.id : undefined}
helperText={i18n.t("Optional")} onRowClick={onRowClick}
disabled={disabled} />
onChange={change} <Card>
value={data.description} <CardTitle
multiline title={intl.formatMessage({
fullWidth defaultMessage: "Photo Information",
description: "section header"
})}
/> />
</CardContent> <CardContent>
</Card> <TextField
</div> name="description"
<div> label={intl.formatMessage(commonMessages.description)}
<Card> helperText={intl.formatMessage({
<CardTitle title={i18n.t("Photo View")} /> defaultMessage: "Optional",
<CardContent> description: "field is optional"
{!!image ? ( })}
<div className={classes.imageContainer}> disabled={disabled}
<img src={image.url} className={classes.image} /> onChange={change}
</div> value={data.description}
) : ( multiline
<Skeleton /> fullWidth
)} />
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</Grid> <div>
<SaveButtonBar <Card>
disabled={disabled || !onSubmit || !hasChanged} <CardTitle
state={saveButtonBarState} title={intl.formatMessage({
onCancel={onBack} defaultMessage: "Photo View",
onDelete={onDelete} description: "section header"
onSave={submit} })}
/> />
</Container> <CardContent>
); {!!image ? (
}} <div className={classes.imageContainer}>
</Form> <img src={image.url} className={classes.image} />
) </div>
) : (
<Skeleton />
)}
</CardContent>
</Card>
</div>
</Grid>
<SaveButtonBar
disabled={disabled || !onSubmit || !hasChanged}
state={saveButtonBarState}
onCancel={onBack}
onDelete={onDelete}
onSave={submit}
/>
</Container>
);
}}
</Form>
);
}
); );
ProductImagePage.displayName = "ProductImagePage"; ProductImagePage.displayName = "ProductImagePage";
export default ProductImagePage; export default ProductImagePage;

View file

@ -2,6 +2,7 @@ import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card"; import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent"; import CardContent from "@material-ui/core/CardContent";
import classNames from "classnames"; import classNames from "classnames";
import { useIntl } from "react-intl";
import { import {
createStyles, createStyles,
@ -12,10 +13,10 @@ import {
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import ImageTile from "@saleor/components/ImageTile"; import ImageTile from "@saleor/components/ImageTile";
import ImageUpload from "@saleor/components/ImageUpload"; import ImageUpload from "@saleor/components/ImageUpload";
import { commonMessages } from "@saleor/intl";
import { ReorderAction } from "@saleor/types"; import { ReorderAction } from "@saleor/types";
import React from "react"; import React from "react";
import { SortableContainer, SortableElement } from "react-sortable-hoc"; import { SortableContainer, SortableElement } from "react-sortable-hoc";
import i18n from "../../../i18n";
import { ProductDetails_product_images } from "../../types/ProductDetails"; import { ProductDetails_product_images } from "../../types/ProductDetails";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
@ -181,75 +182,83 @@ const ProductImages = withStyles(styles, { name: "ProductImages" })(
onImageDelete, onImageDelete,
onImageReorder, onImageReorder,
onImageUpload onImageUpload
}: ProductImagesProps) => ( }: ProductImagesProps) => {
<Card className={classes.card}> const intl = useIntl();
<CardTitle const upload = React.useRef(null);
title={i18n.t("Images")}
toolbar={ return (
<> <Card className={classes.card}>
<Button <CardTitle
onClick={() => this.upload.click()} title={intl.formatMessage({
disabled={loading} defaultMessage: "Images",
variant="text" description: "section header"
color="primary" })}
data-tc="button-upload-image" toolbar={
> <>
{i18n.t("Upload image")} <Button
</Button> onClick={() => upload.current.click()}
<input disabled={loading}
className={classes.fileField} variant="text"
id="fileUpload" color="primary"
onChange={event => onImageUpload(event.target.files[0])} data-tc="button-upload-image"
type="file" >
ref={ref => (this.upload = ref)} {intl.formatMessage(commonMessages.uploadImage)}
/> </Button>
</> <input
} className={classes.fileField}
/> id="fileUpload"
<div className={classes.imageGridContainer}> onChange={event => onImageUpload(event.target.files[0])}
{images === undefined ? ( type="file"
<CardContent> ref={upload}
<div className={classes.root}> />
<div className={classes.imageContainer}> </>
<img className={classes.image} src={placeholderImage} /> }
/>
<div className={classes.imageGridContainer}>
{images === undefined ? (
<CardContent>
<div className={classes.root}>
<div className={classes.imageContainer}>
<img className={classes.image} src={placeholderImage} />
</div>
</div> </div>
</div> </CardContent>
</CardContent> ) : images.length > 0 ? (
) : images.length > 0 ? ( <>
<> <ImageUpload
<ImageUpload className={classes.imageUpload}
className={classes.imageUpload} isActiveClassName={classes.imageUploadActive}
isActiveClassName={classes.imageUploadActive} disableClick={true}
disableClick={true} iconContainerClassName={classes.imageUploadIcon}
iconContainerClassName={classes.imageUploadIcon} iconContainerActiveClassName={classes.imageUploadIconActive}
iconContainerActiveClassName={classes.imageUploadIconActive} onImageUpload={onImageUpload}
onImageUpload={onImageUpload} >
> {({ isDragActive }) => (
{({ isDragActive }) => ( <CardContent>
<CardContent> <ImageListContainer
<ImageListContainer distance={20}
distance={20} helperClass="dragged"
helperClass="dragged" axis="xy"
axis="xy" items={images}
items={images} onSortEnd={onImageReorder}
onSortEnd={onImageReorder} className={classNames({
className={classNames({ [classes.root]: true,
[classes.root]: true, [classes.rootDragActive]: isDragActive
[classes.rootDragActive]: isDragActive })}
})} onImageDelete={onImageDelete}
onImageDelete={onImageDelete} onImageEdit={onImageEdit}
onImageEdit={onImageEdit} />
/> </CardContent>
</CardContent> )}
)} </ImageUpload>
</ImageUpload> </>
</> ) : (
) : ( <ImageUpload onImageUpload={onImageUpload} />
<ImageUpload onImageUpload={onImageUpload} /> )}
)} </div>
</div> </Card>
</Card> );
) }
); );
ProductImages.displayName = "ProductImages"; ProductImages.displayName = "ProductImages";
export default ProductImages; export default ProductImages;

View file

@ -1,8 +1,8 @@
import React from "react"; import React from "react";
import { useIntl } from "react-intl";
import { FieldType, IFilter } from "@saleor/components/Filter"; import { FieldType, IFilter } from "@saleor/components/Filter";
import FilterBar from "@saleor/components/FilterBar"; import FilterBar from "@saleor/components/FilterBar";
import i18n from "@saleor/i18n";
import { FilterProps } from "@saleor/types"; import { FilterProps } from "@saleor/types";
import { StockAvailability } from "@saleor/types/globalTypes"; import { StockAvailability } from "@saleor/types/globalTypes";
import { ProductListUrlFilters } from "../../urls"; import { ProductListUrlFilters } from "../../urls";
@ -17,79 +17,121 @@ export enum ProductFilterKeys {
stock, stock,
query query
} }
const filterMenu: IFilter = [
{
children: [],
data: {
additionalText: i18n.t("is set as"),
fieldLabel: i18n.t("Status"),
options: [
{
label: i18n.t("Visible"),
value: true
},
{
label: i18n.t("Hidden"),
value: false
}
],
type: FieldType.select
},
label: i18n.t("Visibility"),
value: ProductFilterKeys.published.toString()
},
{
children: [],
data: {
fieldLabel: i18n.t("Stock quantity"),
options: [
{
label: i18n.t("Available"),
value: StockAvailability.IN_STOCK
},
{
label: i18n.t("Out Of Stock"),
value: StockAvailability.OUT_OF_STOCK
}
],
type: FieldType.select
},
label: i18n.t("Stock"),
value: ProductFilterKeys.stock.toString()
},
{
children: [
{
children: [],
data: {
additionalText: i18n.t("equals"),
fieldLabel: null,
type: FieldType.price
},
label: i18n.t("Specific Price"),
value: ProductFilterKeys.priceEqual.toString()
},
{
children: [],
data: {
fieldLabel: i18n.t("Range"),
type: FieldType.rangePrice
},
label: i18n.t("Range"),
value: ProductFilterKeys.priceRange.toString()
}
],
data: {
fieldLabel: i18n.t("Price"),
type: FieldType.range
},
label: i18n.t("Price"),
value: ProductFilterKeys.price.toString()
}
];
const ProductListFilter: React.FC<ProductListFilterProps> = props => ( const ProductListFilter: React.FC<ProductListFilterProps> = props => {
<FilterBar {...props} filterMenu={filterMenu} /> const intl = useIntl();
);
const filterMenu: IFilter = [
{
children: [],
data: {
additionalText: intl.formatMessage({
defaultMessage: "is set as",
description: "product status is set as"
}),
fieldLabel: intl.formatMessage({
defaultMessage: "Status",
description: "product status"
}),
options: [
{
label: intl.formatMessage({
defaultMessage: "Visible",
description: "product is visible"
}),
value: true
},
{
label: intl.formatMessage({
defaultMessage: "Hidden",
description: "product is hidden"
}),
value: false
}
],
type: FieldType.select
},
label: intl.formatMessage({
defaultMessage: "Visibility",
description: "product visibility"
}),
value: ProductFilterKeys.published.toString()
},
{
children: [],
data: {
fieldLabel: intl.formatMessage({
defaultMessage: "Stock quantity"
}),
options: [
{
label: intl.formatMessage({
defaultMessage: "Available",
description: "product status"
}),
value: StockAvailability.IN_STOCK
},
{
label: intl.formatMessage({
defaultMessage: "Out Of Stock",
description: "product status"
}),
value: StockAvailability.OUT_OF_STOCK
}
],
type: FieldType.select
},
label: intl.formatMessage({
defaultMessage: "Stock",
description: "product stock"
}),
value: ProductFilterKeys.stock.toString()
},
{
children: [
{
children: [],
data: {
additionalText: intl.formatMessage({
defaultMessage: "equals",
description: "product price"
}),
fieldLabel: null,
type: FieldType.price
},
label: intl.formatMessage({
defaultMessage: "Specific Price"
}),
value: ProductFilterKeys.priceEqual.toString()
},
{
children: [],
data: {
fieldLabel: intl.formatMessage({
defaultMessage: "Range"
}),
type: FieldType.rangePrice
},
label: intl.formatMessage({
defaultMessage: "Range"
}),
value: ProductFilterKeys.priceRange.toString()
}
],
data: {
fieldLabel: intl.formatMessage({
defaultMessage: "Price"
}),
type: FieldType.range
},
label: intl.formatMessage({
defaultMessage: "Price"
}),
value: ProductFilterKeys.price.toString()
}
];
return <FilterBar {...props} filterMenu={filterMenu} />;
};
ProductListFilter.displayName = "ProductListFilter"; ProductListFilter.displayName = "ProductListFilter";
export default ProductListFilter; export default ProductListFilter;

View file

@ -4,6 +4,7 @@ import { Theme } from "@material-ui/core/styles";
import AddIcon from "@material-ui/icons/Add"; import AddIcon from "@material-ui/icons/Add";
import makeStyles from "@material-ui/styles/makeStyles"; import makeStyles from "@material-ui/styles/makeStyles";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { CategoryDetails_category_products_edges_node } from "@saleor/categories/types/CategoryDetails"; import { CategoryDetails_category_products_edges_node } from "@saleor/categories/types/CategoryDetails";
import ColumnPicker, { import ColumnPicker, {
@ -14,7 +15,7 @@ import PageHeader from "@saleor/components/PageHeader";
import ProductList from "@saleor/components/ProductList"; import ProductList from "@saleor/components/ProductList";
import { ProductListColumns } from "@saleor/config"; import { ProductListColumns } from "@saleor/config";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import i18n from "@saleor/i18n"; import { sectionNames } from "@saleor/intl";
import { FilterPageProps, ListActions, PageListProps } from "@saleor/types"; import { FilterPageProps, ListActions, PageListProps } from "@saleor/types";
import { toggle } from "@saleor/utils/lists"; import { toggle } from "@saleor/utils/lists";
import { ProductListUrlFilters } from "../../urls"; import { ProductListUrlFilters } from "../../urls";
@ -53,6 +54,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
onUpdateListSettings, onUpdateListSettings,
...listProps ...listProps
} = props; } = props;
const intl = useIntl();
const classes = useStyles(props); const classes = useStyles(props);
const [selectedColumns, setSelectedColumns] = useStateFromProps( const [selectedColumns, setSelectedColumns] = useStateFromProps(
settings.columns settings.columns
@ -74,22 +76,31 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
const columns: ColumnPickerChoice[] = [ const columns: ColumnPickerChoice[] = [
{ {
label: i18n.t("Published"), label: intl.formatMessage({
defaultMessage: "Published",
description: "product status"
}),
value: "isPublished" as ProductListColumns value: "isPublished" as ProductListColumns
}, },
{ {
label: i18n.t("Price"), label: intl.formatMessage({
defaultMessage: "Price",
description: "product price"
}),
value: "price" as ProductListColumns value: "price" as ProductListColumns
}, },
{ {
label: i18n.t("Type"), label: intl.formatMessage({
defaultMessage: "Type",
description: "product type"
}),
value: "productType" as ProductListColumns value: "productType" as ProductListColumns
} }
]; ];
return ( return (
<Container> <Container>
<PageHeader title={i18n.t("Products")}> <PageHeader title={intl.formatMessage(sectionNames.products)}>
<ColumnPicker <ColumnPicker
className={classes.columnPicker} className={classes.columnPicker}
columns={columns} columns={columns}
@ -105,19 +116,27 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
variant="contained" variant="contained"
data-tc="add-product" data-tc="add-product"
> >
{i18n.t("Add product")} <AddIcon /> <FormattedMessage defaultMessage="Add Product" description="button" />
<AddIcon />
</Button> </Button>
</PageHeader> </PageHeader>
<Card> <Card>
<ProductListFilter <ProductListFilter
allTabLabel={i18n.t("All Products")} allTabLabel={intl.formatMessage({
defaultMessage: "All Products",
description: "tab name"
})}
currencySymbol={currencySymbol} currencySymbol={currencySymbol}
currentTab={currentTab} currentTab={currentTab}
filterLabel={i18n.t("Select all products where:")} filterLabel={intl.formatMessage({
defaultMessage: "Select all products where:"
})}
filterTabs={filterTabs} filterTabs={filterTabs}
filtersList={filtersList} filtersList={filtersList}
initialSearch={initialSearch} initialSearch={initialSearch}
searchPlaceholder={i18n.t("Search Products...")} searchPlaceholder={intl.formatMessage({
defaultMessage: "Search Products..."
})}
onAll={onAll} onAll={onAll}
onSearchChange={onSearchChange} onSearchChange={onSearchChange}
onFilterAdd={onFilterAdd} onFilterAdd={onFilterAdd}

View file

@ -8,6 +8,7 @@ import {
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
@ -20,7 +21,6 @@ import SingleAutocompleteSelectField, {
SingleAutocompleteChoiceType SingleAutocompleteChoiceType
} from "@saleor/components/SingleAutocompleteSelectField"; } from "@saleor/components/SingleAutocompleteSelectField";
import { ChangeEvent } from "@saleor/hooks/useForm"; import { ChangeEvent } from "@saleor/hooks/useForm";
import i18n from "@saleor/i18n";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { FormErrors } from "@saleor/types"; import { FormErrors } from "@saleor/types";
@ -86,79 +86,102 @@ const ProductOrganization = withStyles(styles, { name: "ProductOrganization" })(
onCategoryChange, onCategoryChange,
onCollectionChange, onCollectionChange,
onProductTypeChange onProductTypeChange
}: ProductOrganizationProps) => ( }: ProductOrganizationProps) => {
<Card className={classes.card}> const intl = useIntl();
<CardTitle title={i18n.t("Organize Product")} />
<CardContent> return (
{canChangeType ? ( <Card className={classes.card}>
<SingleAutocompleteSelectField <CardTitle
displayValue={productTypeInputDisplayValue} title={intl.formatMessage({
error={!!errors.productType} defaultMessage: "Organize Product",
helperText={errors.productType} description: "section header"
name="productType" })}
disabled={disabled}
label={i18n.t("Product Type")}
choices={productTypes}
value={data.productType}
onChange={onProductTypeChange}
data-tc="product-type"
/>
) : (
<>
<Typography className={classes.label} variant="caption">
{i18n.t("Product Type")}
</Typography>
<Typography>{maybe(() => productType.name, "...")}</Typography>
<CardSpacer />
<Typography className={classes.label} variant="caption">
{i18n.t("Product Type")}
</Typography>
<Typography>
{maybe(
() =>
productType.hasVariants
? i18n.t("Configurable")
: i18n.t("Simple"),
"..."
)}
</Typography>
</>
)}
<FormSpacer />
<Hr />
<FormSpacer />
<SingleAutocompleteSelectField
displayValue={categoryInputDisplayValue}
error={!!errors.category}
helperText={errors.category}
disabled={disabled}
label={i18n.t("Category")}
choices={disabled ? [] : categories}
name="category"
value={data.category}
onChange={onCategoryChange}
fetchChoices={fetchCategories}
data-tc="category"
/> />
<FormSpacer /> <CardContent>
<Hr /> {canChangeType ? (
<FormSpacer /> <SingleAutocompleteSelectField
<MultiAutocompleteSelectField displayValue={productTypeInputDisplayValue}
displayValues={collectionsInputDisplayValue} error={!!errors.productType}
label={i18n.t("Collections")} helperText={errors.productType}
choices={disabled ? [] : collections} name="productType"
name="collections" disabled={disabled}
value={data.collections} label={intl.formatMessage({
helperText={i18n.t( defaultMessage: "Product Type"
"*Optional. Adding product to collection helps users find it." })}
choices={productTypes}
value={data.productType}
onChange={onProductTypeChange}
data-tc="product-type"
/>
) : (
<>
<Typography className={classes.label} variant="caption">
<FormattedMessage defaultMessage="Product Type" />
</Typography>
<Typography>{maybe(() => productType.name, "...")}</Typography>
<CardSpacer />
<Typography className={classes.label} variant="caption">
<FormattedMessage defaultMessage="Product Type" />
</Typography>
<Typography>
{maybe(
() =>
productType.hasVariants
? intl.formatMessage({
defaultMessage: "Configurable",
description: "product is configurable"
})
: intl.formatMessage({
defaultMessage: "Simple",
description: "product is not configurable"
}),
"..."
)}
</Typography>
</>
)} )}
onChange={onCollectionChange} <FormSpacer />
fetchChoices={fetchCollections} <Hr />
data-tc="collections" <FormSpacer />
/> <SingleAutocompleteSelectField
</CardContent> displayValue={categoryInputDisplayValue}
</Card> error={!!errors.category}
) helperText={errors.category}
disabled={disabled}
label={intl.formatMessage({
defaultMessage: "Category"
})}
choices={disabled ? [] : categories}
name="category"
value={data.category}
onChange={onCategoryChange}
fetchChoices={fetchCategories}
data-tc="category"
/>
<FormSpacer />
<Hr />
<FormSpacer />
<MultiAutocompleteSelectField
displayValues={collectionsInputDisplayValue}
label={intl.formatMessage({
defaultMessage: "Collections"
})}
choices={disabled ? [] : collections}
name="collections"
value={data.collections}
helperText={intl.formatMessage({
defaultMessage:
"*Optional. Adding product to collection helps users find it.",
description: "field is optional"
})}
onChange={onCollectionChange}
fetchChoices={fetchCollections}
data-tc="collections"
/>
</CardContent>
</Card>
);
}
); );
ProductOrganization.displayName = "ProductOrganization"; ProductOrganization.displayName = "ProductOrganization";
export default ProductOrganization; export default ProductOrganization;

View file

@ -7,11 +7,11 @@ import {
WithStyles WithStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import React from "react"; import React from "react";
import { useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox"; import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import PriceField from "@saleor/components/PriceField"; import PriceField from "@saleor/components/PriceField";
import i18n from "../../../i18n";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@ -33,31 +33,45 @@ interface ProductPricingProps extends WithStyles<typeof styles> {
} }
const ProductPricing = withStyles(styles, { name: "ProductPricing" })( const ProductPricing = withStyles(styles, { name: "ProductPricing" })(
({ classes, currency, data, disabled, onChange }: ProductPricingProps) => ( ({ classes, currency, data, disabled, onChange }: ProductPricingProps) => {
<Card> const intl = useIntl();
<CardTitle title={i18n.t("Pricing")}>
<ControlledCheckbox return (
name="chargeTaxes" <Card>
label={i18n.t("Charge taxes for this item")} <CardTitle
checked={data.chargeTaxes} title={intl.formatMessage({
onChange={onChange} defaultMessage: "Pricing",
disabled={disabled} description: "product pricing"
/> })}
</CardTitle> >
<CardContent> <ControlledCheckbox
<div className={classes.root}> name="chargeTaxes"
<PriceField label={intl.formatMessage({
disabled={disabled} defaultMessage: "Charge taxes for this item"
label={i18n.t("Price")} })}
name="basePrice" checked={data.chargeTaxes}
value={data.basePrice}
currencySymbol={currency}
onChange={onChange} onChange={onChange}
disabled={disabled}
/> />
</div> </CardTitle>
</CardContent> <CardContent>
</Card> <div className={classes.root}>
) <PriceField
disabled={disabled}
label={intl.formatMessage({
defaultMessage: "Price",
description: "product price"
})}
name="basePrice"
value={data.basePrice}
currencySymbol={currency}
onChange={onChange}
/>
</div>
</CardContent>
</Card>
);
}
); );
ProductPricing.displayName = "ProductPricing"; ProductPricing.displayName = "ProductPricing";
export default ProductPricing; export default ProductPricing;

View file

@ -8,9 +8,9 @@ import {
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import React from "react"; import React from "react";
import { useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import i18n from "../../../i18n";
import { maybe } from "../../../misc"; import { maybe } from "../../../misc";
import { ProductDetails_product } from "../../types/ProductDetails"; import { ProductDetails_product } from "../../types/ProductDetails";
@ -35,39 +35,70 @@ interface ProductStockProps extends WithStyles<typeof styles> {
} }
const ProductStock = withStyles(styles, { name: "ProductStock" })( const ProductStock = withStyles(styles, { name: "ProductStock" })(
({ classes, data, disabled, product, onChange, errors }: ProductStockProps) => ( ({
<Card> classes,
<CardTitle title={i18n.t("Inventory")} /> data,
<CardContent> disabled,
<div className={classes.root}> product,
<TextField onChange,
disabled={disabled} errors
name="sku" }: ProductStockProps) => {
label={i18n.t("SKU (Stock Keeping Unit)")} const intl = useIntl();
value={data.sku}
onChange={onChange} return (
error={!!errors.sku} <Card>
helperText={errors.sku} <CardTitle
/> title={intl.formatMessage({
<TextField defaultMessage: "Inventory",
disabled={disabled} description: "product stock, section header",
name="stockQuantity" id: "productStockHeader"
label={i18n.t("Inventory")} })}
value={data.stockQuantity} />
type="number" <CardContent>
onChange={onChange} <div className={classes.root}>
helperText={ <TextField
product disabled={disabled}
? i18n.t("Allocated: {{ quantity }}", { name="sku"
quantity: maybe(() => product.variants[0].quantityAllocated) label={intl.formatMessage({
}) defaultMessage: "SKU (Stock Keeping Unit)"
: undefined })}
} value={data.sku}
/> onChange={onChange}
</div> error={!!errors.sku}
</CardContent> helperText={errors.sku}
</Card> />
) <TextField
disabled={disabled}
name="stockQuantity"
label={intl.formatMessage({
defaultMessage: "Inventory",
description: "product stock",
id: "prodictStockInventoryLabel"
})}
value={data.stockQuantity}
type="number"
onChange={onChange}
helperText={
product
? intl.formatMessage(
{
defaultMessage: "Allocated: {quantity}",
description: "allocated product stock"
},
{
quantity: maybe(
() => product.variants[0].quantityAllocated
)
}
)
: undefined
}
/>
</div>
</CardContent>
</Card>
);
}
); );
ProductStock.displayName = "ProductStock"; ProductStock.displayName = "ProductStock";
export default ProductStock; export default ProductStock;

View file

@ -1,5 +1,6 @@
import { convertFromRaw, RawDraftContentState } from "draft-js"; import { convertFromRaw, RawDraftContentState } from "draft-js";
import React from "react"; import React from "react";
import { useIntl } from "react-intl";
import AppHeader from "@saleor/components/AppHeader"; import AppHeader from "@saleor/components/AppHeader";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
@ -15,7 +16,7 @@ import { SearchCategories_categories_edges_node } from "@saleor/containers/Searc
import { SearchCollections_collections_edges_node } from "@saleor/containers/SearchCollections/types/SearchCollections"; import { SearchCollections_collections_edges_node } from "@saleor/containers/SearchCollections/types/SearchCollections";
import useFormset from "@saleor/hooks/useFormset"; import useFormset from "@saleor/hooks/useFormset";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import i18n from "@saleor/i18n"; import { sectionNames } from "@saleor/intl";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { ListActions, UserError } from "@saleor/types"; import { ListActions, UserError } from "@saleor/types";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
@ -107,6 +108,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
toggleAll, toggleAll,
toolbar toolbar
}) => { }) => {
const intl = useIntl();
const attributeInput = React.useMemo( const attributeInput = React.useMemo(
() => getAttributeInputFromProduct(product), () => getAttributeInputFromProduct(product),
[product] [product]
@ -188,7 +190,9 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
return ( return (
<> <>
<Container> <Container>
<AppHeader onBack={onBack}>{i18n.t("Products")}</AppHeader> <AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.products)}
</AppHeader>
<PageHeader title={header} /> <PageHeader title={header} />
<Grid> <Grid>
<div> <div>

View file

@ -1,6 +1,7 @@
import Card from "@material-ui/core/Card"; import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent"; import CardContent from "@material-ui/core/CardContent";
import React from "react"; import React from "react";
import { useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
@ -9,7 +10,7 @@ import SingleAutocompleteSelectField, {
} from "@saleor/components/SingleAutocompleteSelectField"; } from "@saleor/components/SingleAutocompleteSelectField";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset"; import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset";
import i18n from "../../../i18n"; import { commonMessages } from "@saleor/intl";
import { ProductVariant_attributes_attribute_values } from "../../types/ProductVariant"; import { ProductVariant_attributes_attribute_values } from "../../types/ProductVariant";
export interface VariantAttributeInputData { export interface VariantAttributeInputData {
@ -67,40 +68,46 @@ const ProductVariantAttributes: React.FC<ProductVariantAttributesProps> = ({
disabled, disabled,
errors, errors,
onChange onChange
}) => ( }) => {
<Card> const intl = useIntl();
<CardTitle title={i18n.t("General Information")} />
<CardContent> return (
<Grid variant="uniform"> <Card>
{attributes === undefined ? ( <CardTitle
<Skeleton /> title={intl.formatMessage(commonMessages.generalInformations)}
) : ( />
attributes.map((attribute, attributeIndex) => { <CardContent>
return ( <Grid variant="uniform">
<SingleAutocompleteSelectField {attributes === undefined ? (
key={attributeIndex} <Skeleton />
disabled={disabled} ) : (
displayValue={getAttributeDisplayValue( attributes.map((attribute, attributeIndex) => {
attribute.id, return (
attribute.value, <SingleAutocompleteSelectField
attributes key={attributeIndex}
)} disabled={disabled}
error={!!errors[attribute.id]} displayValue={getAttributeDisplayValue(
helperText={errors[attribute.id]} attribute.id,
label={attribute.label} attribute.value,
name={`attribute:${attribute.id}`} attributes
onChange={event => onChange(attribute.id, event.target.value)} )}
value={getAttributeValue(attribute.id, attributes)} error={!!errors[attribute.id]}
choices={getAttributeValueChoices(attribute.id, attributes)} helperText={errors[attribute.id]}
allowCustomValues label={attribute.label}
data-tc="variant-attribute-input" name={`attribute:${attribute.id}`}
/> onChange={event => onChange(attribute.id, event.target.value)}
); value={getAttributeValue(attribute.id, attributes)}
}) choices={getAttributeValueChoices(attribute.id, attributes)}
)} allowCustomValues
</Grid> data-tc="variant-attribute-input"
</CardContent> />
</Card> );
); })
)}
</Grid>
</CardContent>
</Card>
);
};
ProductVariantAttributes.displayName = "ProductVariantAttributes"; ProductVariantAttributes.displayName = "ProductVariantAttributes";
export default ProductVariantAttributes; export default ProductVariantAttributes;

View file

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { useIntl } from "react-intl";
import AppHeader from "@saleor/components/AppHeader"; import AppHeader from "@saleor/components/AppHeader";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
@ -16,7 +17,6 @@ import {
getVariantAttributeErrors, getVariantAttributeErrors,
getVariantAttributeInputFromProduct getVariantAttributeInputFromProduct
} from "@saleor/products/utils/data"; } from "@saleor/products/utils/data";
import i18n from "../../../i18n";
import { maybe } from "../../../misc"; import { maybe } from "../../../misc";
import { UserError } from "../../../types"; import { UserError } from "../../../types";
import { ProductVariantCreateData_product } from "../../types/ProductVariantCreateData"; import { ProductVariantCreateData_product } from "../../types/ProductVariantCreateData";
@ -63,6 +63,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
onSubmit, onSubmit,
onVariantClick onVariantClick
}) => { }) => {
const intl = useIntl();
const attributeInput = React.useMemo( const attributeInput = React.useMemo(
() => getVariantAttributeInputFromProduct(product), () => getVariantAttributeInputFromProduct(product),
[product] [product]
@ -150,8 +151,14 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
<SaveButtonBar <SaveButtonBar
disabled={loading || !onSubmit || !hasChanged} disabled={loading || !onSubmit || !hasChanged}
labels={{ labels={{
delete: i18n.t("Remove variant"), delete: intl.formatMessage({
save: i18n.t("Save variant") defaultMessage: "Delete Variant",
description: "button"
}),
save: intl.formatMessage({
defaultMessage: "Save variant",
description: "button"
})
}} }}
state={saveButtonBarState} state={saveButtonBarState}
onCancel={onBack} onCancel={onBack}

View file

@ -11,11 +11,12 @@ import {
WithStyles WithStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import React from "react"; import React from "react";
import { FormattedMessage } from "react-intl";
import ConfirmButton, { import ConfirmButton, {
ConfirmButtonTransitionState ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton"; } from "@saleor/components/ConfirmButton";
import i18n from "../../../i18n"; import { buttonMessages } from "@saleor/intl";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@ -50,21 +51,25 @@ const ProductVariantDeleteDialog = withStyles(styles, {
}: ProductVariantDeleteDialogProps) => ( }: ProductVariantDeleteDialogProps) => (
<Dialog onClose={onClose} open={open}> <Dialog onClose={onClose} open={open}>
<DialogTitle> <DialogTitle>
{i18n.t("Delete variant", { context: "title" })} <FormattedMessage
defaultMessage="Delete Variant"
description="dialog header"
/>
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText <DialogContentText>
dangerouslySetInnerHTML={{ <FormattedMessage
__html: i18n.t( defaultMessage="Are you sure you want to delete {name}?"
"Are you sure you want to remove <strong>{{name}}</strong>?", description="delete product variant"
{ name } values={{
) name
}} }}
/> />
</DialogContentText>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onClose}> <Button onClick={onClose}>
{i18n.t("Cancel", { context: "button" })} <FormattedMessage {...buttonMessages.cancel} />
</Button> </Button>
<ConfirmButton <ConfirmButton
transitionState={confirmButtonState} transitionState={confirmButtonState}
@ -72,7 +77,10 @@ const ProductVariantDeleteDialog = withStyles(styles, {
variant="contained" variant="contained"
onClick={onConfirm} onClick={onConfirm}
> >
{i18n.t("Delete variant", { context: "button" })} <FormattedMessage
defaultMessage="Delete variant"
description="button"
/>
</ConfirmButton> </ConfirmButton>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View file

@ -11,8 +11,9 @@ import {
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import { FormattedMessage } from "react-intl";
import i18n from "../../../i18n"; import { buttonMessages } from "@saleor/intl";
import { ProductImage } from "../../types/ProductImage"; import { ProductImage } from "../../types/ProductImage";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
@ -72,7 +73,10 @@ const ProductVariantImageSelectDialog = withStyles(styles, {
}: ProductVariantImageSelectDialogProps) => ( }: ProductVariantImageSelectDialogProps) => (
<Dialog onClose={onClose} open={open}> <Dialog onClose={onClose} open={open}>
<DialogTitle> <DialogTitle>
{i18n.t("Image selection", { context: "title" })} <FormattedMessage
defaultMessage="Image Selection"
description="dialog header"
/>
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<div className={classes.root}> <div className={classes.root}>
@ -97,7 +101,7 @@ const ProductVariantImageSelectDialog = withStyles(styles, {
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onClose}> <Button onClick={onClose}>
{i18n.t("Close", { context: "button" })} <FormattedMessage {...buttonMessages.back} />
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View file

@ -9,10 +9,10 @@ import {
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import i18n from "../../../i18n";
import { ProductImage } from "../../types/ProductImage"; import { ProductImage } from "../../types/ProductImage";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
@ -54,41 +54,51 @@ interface ProductVariantImagesProps extends WithStyles<typeof styles> {
export const ProductVariantImages = withStyles(styles, { export const ProductVariantImages = withStyles(styles, {
name: "ProductVariantImages" name: "ProductVariantImages"
})(({ classes, disabled, images, onImageAdd }: ProductVariantImagesProps) => ( })(({ classes, disabled, images, onImageAdd }: ProductVariantImagesProps) => {
<Card> const intl = useIntl();
<CardTitle
title={i18n.t("Images")} return (
toolbar={ <Card>
<Button <CardTitle
color="primary" title={intl.formatMessage({
variant="text" defaultMessage: "Images",
disabled={disabled} description: "section header"
onClick={onImageAdd} })}
> toolbar={
{i18n.t("Choose photos")} <Button
</Button> color="primary"
} variant="text"
/> disabled={disabled}
<CardContent> onClick={onImageAdd}
<div className={classes.root}> >
{images === undefined || images === null ? ( <FormattedMessage
<Skeleton /> defaultMessage="Choose photos"
) : images.length > 0 ? ( description="button"
images />
.sort((prev, next) => (prev.sortOrder > next.sortOrder ? 1 : -1)) </Button>
.map(tile => ( }
<div className={classes.imageContainer} key={tile.id}> />
<img className={classes.image} src={tile.url} /> <CardContent>
</div> <div className={classes.root}>
)) {images === undefined || images === null ? (
) : ( <Skeleton />
<Typography className={classes.helpText}> ) : images.length > 0 ? (
{i18n.t("Select a specific variant image from product images")} images
</Typography> .sort((prev, next) => (prev.sortOrder > next.sortOrder ? 1 : -1))
)} .map(tile => (
</div> <div className={classes.imageContainer} key={tile.id}>
</CardContent> <img className={classes.image} src={tile.url} />
</Card> </div>
)); ))
) : (
<Typography className={classes.helpText}>
<FormattedMessage defaultMessage="Select a specific variant image from product images" />
</Typography>
)}
</div>
</CardContent>
</Card>
);
});
ProductVariantImages.displayName = "ProductVariantImages"; ProductVariantImages.displayName = "ProductVariantImages";
export default ProductVariantImages; export default ProductVariantImages;

View file

@ -12,11 +12,11 @@ import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow"; import TableRow from "@material-ui/core/TableRow";
import classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import TableCellAvatar from "@saleor/components/TableCellAvatar"; import TableCellAvatar from "@saleor/components/TableCellAvatar";
import i18n from "../../../i18n";
import { maybe, renderCollection } from "../../../misc"; import { maybe, renderCollection } from "../../../misc";
import { ProductVariantCreateData_product_variants } from "../../types/ProductVariantCreateData"; import { ProductVariantCreateData_product_variants } from "../../types/ProductVariantCreateData";
import { ProductVariantDetails_productVariant } from "../../types/ProductVariantDetails"; import { ProductVariantDetails_productVariant } from "../../types/ProductVariantDetails";
@ -63,52 +63,70 @@ const ProductVariantNavigation = withStyles(styles, {
variants, variants,
onAdd, onAdd,
onRowClick onRowClick
}: ProductVariantNavigationProps) => ( }: ProductVariantNavigationProps) => {
<Card> const intl = useIntl();
<CardTitle title={i18n.t("Variants")} />
<Table> return (
<TableBody> <Card>
{renderCollection(variants, variant => ( <CardTitle
<TableRow title={intl.formatMessage({
hover={!!variant} defaultMessage: "Variants",
key={variant ? variant.id : "skeleton"} description: "section header"
className={classes.link} })}
onClick={variant ? () => onRowClick(variant.id) : undefined} />
> <Table>
<TableCellAvatar <TableBody>
className={classNames({ {renderCollection(variants, variant => (
[classes.tabActive]: variant && variant.id === current <TableRow
})} hover={!!variant}
thumbnail={maybe( key={variant ? variant.id : "skeleton"}
() => variant.images[0].url, className={classes.link}
fallbackThumbnail onClick={variant ? () => onRowClick(variant.id) : undefined}
)} >
/> <TableCellAvatar
<TableCell className={classes.textLeft}> className={classNames({
{variant ? variant.name || variant.sku : <Skeleton />} [classes.tabActive]: variant && variant.id === current
</TableCell> })}
</TableRow> thumbnail={maybe(
))} () => variant.images[0].url,
{onAdd ? ( fallbackThumbnail
<TableRow> )}
<TableCell colSpan={2}> />
<Button color="primary" onClick={onAdd}> <TableCell className={classes.textLeft}>
{i18n.t("Add variant")} {variant ? variant.name || variant.sku : <Skeleton />}
</Button> </TableCell>
</TableCell> </TableRow>
</TableRow> ))}
) : ( {onAdd ? (
<TableRow> <TableRow>
<TableCellAvatar className={classes.tabActive} thumbnail={null} /> <TableCell colSpan={2}>
<TableCell className={classes.textLeft}> <Button color="primary" onClick={onAdd}>
{i18n.t("New Variant")} <FormattedMessage
</TableCell> defaultMessage="Add variant"
</TableRow> description="button"
)} />
</TableBody> </Button>
</Table> </TableCell>
</Card> </TableRow>
) ) : (
<TableRow>
<TableCellAvatar
className={classes.tabActive}
thumbnail={null}
/>
<TableCell className={classes.textLeft}>
<FormattedMessage
defaultMessage="New Variant"
description="variant name"
/>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</Card>
);
}
); );
ProductVariantNavigation.displayName = "ProductVariantNavigation"; ProductVariantNavigation.displayName = "ProductVariantNavigation";
export default ProductVariantNavigation; export default ProductVariantNavigation;

View file

@ -7,10 +7,10 @@ import {
WithStyles WithStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import React from "react"; import React from "react";
import { useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import PriceField from "@saleor/components/PriceField"; import PriceField from "@saleor/components/PriceField";
import i18n from "../../../i18n";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@ -39,43 +39,68 @@ const ProductVariantPrice = withStyles(styles, { name: "ProductVariantPrice" })(
priceOverride, priceOverride,
loading, loading,
onChange onChange
}: ProductVariantPriceProps) => ( }: ProductVariantPriceProps) => {
<Card> const intl = useIntl();
<CardTitle title={i18n.t("Pricing")} />
<CardContent> return (
<div className={classes.grid}> <Card>
<div> <CardTitle
<PriceField title={intl.formatMessage({
error={!!errors.price_override} defaultMessage: "Pricing",
name="priceOverride" description: "product pricing, section header"
label={i18n.t("Selling price override")} })}
hint={ />
errors.price_override <CardContent>
? errors.price_override <div className={classes.grid}>
: i18n.t("Optional") <div>
} <PriceField
value={priceOverride} error={!!errors.price_override}
currencySymbol={currencySymbol} name="priceOverride"
onChange={onChange} label={intl.formatMessage({
disabled={loading} defaultMessage: "Selling price override"
/> })}
hint={
errors.price_override
? errors.price_override
: intl.formatMessage({
defaultMessage: "Optional",
description: "optional field",
id: "productVariantPriceOptionalPriceOverrideField"
})
}
value={priceOverride}
currencySymbol={currencySymbol}
onChange={onChange}
disabled={loading}
/>
</div>
<div>
<PriceField
error={!!errors.cost_price}
name="costPrice"
label={intl.formatMessage({
defaultMessage: "Cost price override"
})}
hint={
errors.cost_price
? errors.cost_price
: intl.formatMessage({
defaultMessage: "Optional",
description: "optional field",
id: "productVariantPriceOptionalCostPriceField"
})
}
value={costPrice}
currencySymbol={currencySymbol}
onChange={onChange}
disabled={loading}
/>
</div>
</div> </div>
<div> </CardContent>
<PriceField </Card>
error={!!errors.cost_price} );
name="costPrice" }
label={i18n.t("Cost price override")}
hint={errors.cost_price ? errors.cost_price : i18n.t("Optional")}
value={costPrice}
currencySymbol={currencySymbol}
onChange={onChange}
disabled={loading}
/>
</div>
</div>
</CardContent>
</Card>
)
); );
ProductVariantPrice.displayName = "ProductVariantPrice"; ProductVariantPrice.displayName = "ProductVariantPrice";
export default ProductVariantPrice; export default ProductVariantPrice;

View file

@ -8,9 +8,9 @@ import {
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import React from "react"; import React from "react";
import { useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import i18n from "../../../i18n";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@ -42,48 +42,67 @@ const ProductVariantStock = withStyles(styles, { name: "ProductVariantStock" })(
stockAllocated, stockAllocated,
loading, loading,
onChange onChange
}: ProductVariantStockProps) => ( }: ProductVariantStockProps) => {
<Card> const intl = useIntl();
<CardTitle title={i18n.t("Stock")} />
<CardContent> return (
<div className={classes.grid}> <Card>
<div> <CardTitle
<TextField title={intl.formatMessage({
error={!!errors.quantity} defaultMessage: "Stock",
name="quantity" description: "product variant stock, section header"
value={quantity} })}
label={i18n.t("Inventory")} />
helperText={ <CardContent>
errors.quantity <div className={classes.grid}>
? errors.quantity <div>
: !!stockAllocated <TextField
? i18n.t("Allocated: {{ quantity }}", { error={!!errors.quantity}
context: "variant allocated stock", name="quantity"
quantity: stockAllocated value={quantity}
}) label={intl.formatMessage({
: undefined defaultMessage: "Inventory",
} description: "product variant stock"
onChange={onChange} })}
disabled={loading} helperText={
fullWidth errors.quantity
/> ? errors.quantity
: !!stockAllocated
? intl.formatMessage(
{
defaultMessage: "Allocated: {quantity}",
description: "variant allocated stock"
},
{
quantity: stockAllocated
}
)
: undefined
}
onChange={onChange}
disabled={loading}
fullWidth
/>
</div>
<div>
<TextField
error={!!errors.sku}
helperText={errors.sku}
name="sku"
value={sku}
label={intl.formatMessage({
defaultMessage: "SKU (Stock Keeping Unit)"
})}
onChange={onChange}
disabled={loading}
fullWidth
/>
</div>
</div> </div>
<div> </CardContent>
<TextField </Card>
error={!!errors.sku} );
helperText={errors.sku} }
name="sku"
value={sku}
label={i18n.t("SKU (Stock Keeping Unit)")}
onChange={onChange}
disabled={loading}
fullWidth
/>
</div>
</div>
</CardContent>
</Card>
)
); );
ProductVariantStock.displayName = "ProductVariantStock"; ProductVariantStock.displayName = "ProductVariantStock";
export default ProductVariantStock; export default ProductVariantStock;

View file

@ -14,6 +14,7 @@ import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow"; import TableRow from "@material-ui/core/TableRow";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import Checkbox from "@saleor/components/Checkbox"; import Checkbox from "@saleor/components/Checkbox";
@ -21,7 +22,6 @@ import Money from "@saleor/components/Money";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import StatusLabel from "@saleor/components/StatusLabel"; import StatusLabel from "@saleor/components/StatusLabel";
import TableHead from "@saleor/components/TableHead"; import TableHead from "@saleor/components/TableHead";
import i18n from "../../../i18n";
import { renderCollection } from "../../../misc"; import { renderCollection } from "../../../misc";
import { ListActions } from "../../../types"; import { ListActions } from "../../../types";
import { ProductDetails_product_variants } from "../../types/ProductDetails"; import { ProductDetails_product_variants } from "../../types/ProductDetails";
@ -88,130 +88,162 @@ export const ProductVariants = withStyles(styles, { name: "ProductVariants" })(
toggle, toggle,
toggleAll, toggleAll,
toolbar toolbar
}: ProductVariantsProps) => ( }: ProductVariantsProps) => {
<Card> const intl = useIntl();
<CardTitle
title={i18n.t("Variants")}
toolbar={
<>
<Button
onClick={onAttributesEdit}
variant="text"
color="primary"
data-tc="button-edit-attributes"
>
{i18n.t("Edit attributes")}
</Button>
<Button
onClick={onVariantAdd}
variant="text"
color="primary"
data-tc="button-add-variant"
>
{i18n.t("Add variant")}
</Button>
</>
}
/>
<CardContent>
<Typography>
{i18n.t(
"Use variants for products that come in a variety of versions for example different sizes or colors"
)}
</Typography>
</CardContent>
<Table className={classes.denseTable}>
<TableHead
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={variants}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCell className={classes.colName}>{i18n.t("Name")}</TableCell>
<TableCell className={classes.colStatus}>
{i18n.t("Status")}
</TableCell>
<TableCell className={classes.colSku}>{i18n.t("SKU")}</TableCell>
<Hidden smDown>
<TableCell className={classes.colPrice}>
{i18n.t("Price")}
</TableCell>
</Hidden>
</TableHead>
<TableBody>
{renderCollection(
variants,
variant => {
const isSelected = variant ? isChecked(variant.id) : false;
return ( return (
<TableRow <Card>
selected={isSelected} <CardTitle
hover={!!variant} title={intl.formatMessage({
onClick={onRowClick(variant.id)} defaultMessage: "Variants",
key={variant ? variant.id : "skeleton"} description: "section header"
className={classes.link} })}
> toolbar={
<TableCell padding="checkbox"> <>
<Checkbox <Button
checked={isSelected} onClick={onAttributesEdit}
disabled={disabled} variant="text"
disableClickPropagation color="primary"
onChange={() => toggle(variant.id)} data-tc="button-edit-attributes"
/> >
</TableCell> <FormattedMessage
<TableCell className={classes.colName}> defaultMessage="Edit attributes"
{variant ? variant.name || variant.sku : <Skeleton />} description="product variant attributes, button"
</TableCell> />
<TableCell className={classes.colStatus}> </Button>
{variant ? ( <Button
<StatusLabel onClick={onVariantAdd}
status={variant.stockQuantity > 0 ? "success" : "error"} variant="text"
label={ color="primary"
variant.stockQuantity > 0 data-tc="button-add-variant"
? i18n.t("Available") >
: i18n.t("Unavailable") <FormattedMessage
} defaultMessage="Add variant"
description="button"
/>
</Button>
</>
}
/>
<CardContent>
<Typography>
<FormattedMessage defaultMessage="Use variants for products that come in a variety of versions for example different sizes or colors" />
</Typography>
</CardContent>
<Table className={classes.denseTable}>
<TableHead
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={variants}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCell className={classes.colName}>
<FormattedMessage
defaultMessage="Name"
description="product variant name"
/>
</TableCell>
<TableCell className={classes.colStatus}>
<FormattedMessage
defaultMessage="Status"
description="product variant status"
/>
</TableCell>
<TableCell className={classes.colSku}>
<FormattedMessage defaultMessage="SKU" />
</TableCell>
<Hidden smDown>
<TableCell className={classes.colPrice}>
<FormattedMessage
defaultMessage="Price"
description="product variant price"
/>
</TableCell>
</Hidden>
</TableHead>
<TableBody>
{renderCollection(
variants,
variant => {
const isSelected = variant ? isChecked(variant.id) : false;
return (
<TableRow
selected={isSelected}
hover={!!variant}
onClick={onRowClick(variant.id)}
key={variant ? variant.id : "skeleton"}
className={classes.link}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(variant.id)}
/> />
) : ( </TableCell>
<Skeleton /> <TableCell className={classes.colName}>
)} {variant ? variant.name || variant.sku : <Skeleton />}
</TableCell> </TableCell>
<TableCell className={classes.colSku}> <TableCell className={classes.colStatus}>
{variant ? variant.sku : <Skeleton />}
</TableCell>
<Hidden smDown>
<TableCell className={classes.colPrice}>
{variant ? ( {variant ? (
variant.priceOverride ? ( <StatusLabel
<Money money={variant.priceOverride} /> status={
) : fallbackPrice ? ( variant.stockQuantity > 0 ? "success" : "error"
<Money money={fallbackPrice} /> }
) : ( label={
<Skeleton /> variant.stockQuantity > 0
) ? intl.formatMessage({
defaultMessage: "Available",
description: "product variant status"
})
: intl.formatMessage({
defaultMessage: "Unavailable",
description: "product variant status"
})
}
/>
) : ( ) : (
<Skeleton /> <Skeleton />
)} )}
</TableCell> </TableCell>
</Hidden> <TableCell className={classes.colSku}>
{variant ? variant.sku : <Skeleton />}
</TableCell>
<Hidden smDown>
<TableCell className={classes.colPrice}>
{variant ? (
variant.priceOverride ? (
<Money money={variant.priceOverride} />
) : fallbackPrice ? (
<Money money={fallbackPrice} />
) : (
<Skeleton />
)
) : (
<Skeleton />
)}
</TableCell>
</Hidden>
</TableRow>
);
},
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage defaultMessage="This product has no variants" />
</TableCell>
</TableRow> </TableRow>
); )
}, )}
() => ( </TableBody>
<TableRow> </Table>
<TableCell colSpan={numberOfColumns}> </Card>
{i18n.t("This product has no variants")} );
</TableCell> }
</TableRow>
)
)}
</TableBody>
</Table>
</Card>
)
); );
ProductVariants.displayName = "ProductVariants"; ProductVariants.displayName = "ProductVariants";
export default ProductVariants; export default ProductVariants;

View file

@ -1,9 +1,10 @@
import { parse as parseQs } from "qs"; import { parse as parseQs } from "qs";
import React from "react"; import React from "react";
import { useIntl } from "react-intl";
import { Route, RouteComponentProps, Switch } from "react-router-dom"; import { Route, RouteComponentProps, Switch } from "react-router-dom";
import { sectionNames } from "@saleor/intl";
import { WindowTitle } from "../components/WindowTitle"; import { WindowTitle } from "../components/WindowTitle";
import i18n from "../i18n";
import { import {
productAddPath, productAddPath,
productImagePath, productImagePath,
@ -86,28 +87,32 @@ const ProductVariantCreate: React.StatelessComponent<
); );
}; };
const Component = () => ( const Component = () => {
<> const intl = useIntl();
<WindowTitle title={i18n.t("Products")} />
<Switch> return (
<Route exact path={productListPath} component={ProductList} /> <>
<Route exact path={productAddPath} component={ProductCreate} /> <WindowTitle title={intl.formatMessage(sectionNames.products)} />
<Route <Switch>
exact <Route exact path={productListPath} component={ProductList} />
path={productVariantAddPath(":id")} <Route exact path={productAddPath} component={ProductCreate} />
component={ProductVariantCreate} <Route
/> exact
<Route path={productVariantAddPath(":id")}
path={productVariantEditPath(":productId", ":variantId")} component={ProductVariantCreate}
component={ProductVariant} />
/> <Route
<Route path={productVariantEditPath(":productId", ":variantId")}
path={productImagePath(":productId", ":imageId")} component={ProductVariant}
component={ProductImage} />
/> <Route
<Route path={productPath(":id")} component={ProductUpdate} /> path={productImagePath(":productId", ":imageId")}
</Switch> component={ProductImage}
</> />
); <Route path={productPath(":id")} component={ProductUpdate} />
</Switch>
</>
);
};
export default Component; export default Component;

View file

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { useIntl } from "react-intl";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
@ -7,7 +8,6 @@ import useShop from "@saleor/hooks/useShop";
import { DEFAULT_INITIAL_SEARCH_DATA } from "../../config"; import { DEFAULT_INITIAL_SEARCH_DATA } from "../../config";
import SearchCategories from "../../containers/SearchCategories"; import SearchCategories from "../../containers/SearchCategories";
import SearchCollections from "../../containers/SearchCollections"; import SearchCollections from "../../containers/SearchCollections";
import i18n from "../../i18n";
import { decimal, getMutationState, maybe } from "../../misc"; import { decimal, getMutationState, maybe } from "../../misc";
import ProductCreatePage, { import ProductCreatePage, {
ProductCreatePageSubmitData ProductCreatePageSubmitData
@ -27,6 +27,7 @@ export const ProductUpdate: React.StatelessComponent<
const navigate = useNavigator(); const navigate = useNavigator();
const notify = useNotifier(); const notify = useNotifier();
const shop = useShop(); const shop = useShop();
const intl = useIntl();
const handleAttributesEdit = undefined; const handleAttributesEdit = undefined;
const handleBack = () => navigate(productListUrl()); const handleBack = () => navigate(productListUrl());
@ -41,7 +42,9 @@ export const ProductUpdate: React.StatelessComponent<
const handleSuccess = (data: ProductCreate) => { const handleSuccess = (data: ProductCreate) => {
if (data.productCreate.errors.length === 0) { if (data.productCreate.errors.length === 0) {
notify({ notify({
text: i18n.t("Product created") text: intl.formatMessage({
defaultMessage: "Product created"
})
}); });
navigate(productUrl(data.productCreate.product.id)); navigate(productUrl(data.productCreate.product.id));
} else { } else {
@ -109,7 +112,12 @@ export const ProductUpdate: React.StatelessComponent<
); );
return ( return (
<> <>
<WindowTitle title={i18n.t("Create product")} /> <WindowTitle
title={intl.formatMessage({
defaultMessage: "Create Product",
description: "window title"
})}
/>
<ProductCreatePage <ProductCreatePage
currency={maybe(() => shop.defaultCurrency)} currency={maybe(() => shop.defaultCurrency)}
categories={maybe( categories={maybe(
@ -127,7 +135,10 @@ export const ProductUpdate: React.StatelessComponent<
)} )}
fetchCategories={searchCategory} fetchCategories={searchCategory}
fetchCollections={searchCollection} fetchCollections={searchCollection}
header={i18n.t("New Product")} header={intl.formatMessage({
defaultMessage: "New Product",
description: "page header"
})}
productTypes={maybe(() => productTypes={maybe(() =>
data.productTypes.edges.map(edge => edge.node) data.productTypes.edges.map(edge => edge.node)
)} )}

View file

@ -1,10 +1,10 @@
import DialogContentText from "@material-ui/core/DialogContentText"; import DialogContentText from "@material-ui/core/DialogContentText";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import i18n from "../../i18n";
import { getMutationState, maybe } from "../../misc"; import { getMutationState, maybe } from "../../misc";
import ProductImagePage from "../components/ProductImagePage"; import ProductImagePage from "../components/ProductImagePage";
import { import {
@ -32,6 +32,7 @@ export const ProductImage: React.StatelessComponent<ProductImageProps> = ({
}) => { }) => {
const navigate = useNavigator(); const navigate = useNavigator();
const notify = useNotifier(); const notify = useNotifier();
const intl = useIntl();
const handleBack = () => navigate(productUrl(productId)); const handleBack = () => navigate(productUrl(productId));
const handleUpdateSuccess = (data: ProductImageUpdate) => { const handleUpdateSuccess = (data: ProductImageUpdate) => {
@ -103,19 +104,15 @@ export const ProductImage: React.StatelessComponent<ProductImageProps> = ({
} }
onConfirm={handleDelete} onConfirm={handleDelete}
open={params.action === "remove"} open={params.action === "remove"}
title={i18n.t("Remove image", { title={intl.formatMessage({
context: "modal title" defaultMessage: "Remove Image",
description: "dialog header"
})} })}
variant="delete" variant="delete"
confirmButtonState={deleteTransitionState} confirmButtonState={deleteTransitionState}
> >
<DialogContentText> <DialogContentText>
{i18n.t( <FormattedMessage defaultMessage="Are you sure you want to remove this image?" />
"Are you sure you want to remove this image?",
{
context: "modal content"
}
)}
</DialogContentText> </DialogContentText>
</ActionDialog> </ActionDialog>
</> </>

View file

@ -3,6 +3,7 @@ import DialogContentText from "@material-ui/core/DialogContentText";
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton";
import DeleteIcon from "@material-ui/icons/Delete"; import DeleteIcon from "@material-ui/icons/Delete";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog"; import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog";
@ -19,7 +20,6 @@ import usePaginator, {
createPaginationState createPaginationState
} from "@saleor/hooks/usePaginator"; } from "@saleor/hooks/usePaginator";
import useShop from "@saleor/hooks/useShop"; import useShop from "@saleor/hooks/useShop";
import i18n from "@saleor/i18n";
import { getMutationState, maybe } from "@saleor/misc"; import { getMutationState, maybe } from "@saleor/misc";
import { ListViews } from "@saleor/types"; import { ListViews } from "@saleor/types";
import ProductListPage from "../../components/ProductListPage"; import ProductListPage from "../../components/ProductListPage";
@ -67,6 +67,8 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
const { updateListSettings, settings } = useListSettings<ProductListColumns>( const { updateListSettings, settings } = useListSettings<ProductListColumns>(
ListViews.PRODUCT_LIST ListViews.PRODUCT_LIST
); );
const intl = useIntl();
const tabs = getFilterTabs(); const tabs = getFilterTabs();
const currentTab = const currentTab =
@ -155,7 +157,9 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
if (data.productBulkDelete.errors.length === 0) { if (data.productBulkDelete.errors.length === 0) {
closeModal(); closeModal();
notify({ notify({
text: i18n.t("Products removed") text: intl.formatMessage({
defaultMessage: "Products removed"
})
}); });
reset(); reset();
refetch(); refetch();
@ -166,7 +170,10 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
if (data.productBulkPublish.errors.length === 0) { if (data.productBulkPublish.errors.length === 0) {
closeModal(); closeModal();
notify({ notify({
text: i18n.t("Changed publication status") text: intl.formatMessage({
defaultMessage: "Changed publication status",
description: "product status update notification"
})
}); });
reset(); reset();
refetch(); refetch();
@ -235,13 +242,19 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
openModal("unpublish", listElements) openModal("unpublish", listElements)
} }
> >
{i18n.t("Unpublish")} <FormattedMessage
defaultMessage="Unpublish"
description="unpublish product, button"
/>
</Button> </Button>
<Button <Button
color="primary" color="primary"
onClick={() => openModal("publish", listElements)} onClick={() => openModal("publish", listElements)}
> >
{i18n.t("Publish")} <FormattedMessage
defaultMessage="Publish"
description="publish product, button"
/>
</Button> </Button>
<IconButton <IconButton
color="primary" color="primary"
@ -274,22 +287,29 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
variables: { ids: params.ids } variables: { ids: params.ids }
}) })
} }
title={i18n.t("Remove products")} title={intl.formatMessage({
defaultMessage: "Delete Products",
description: "dialog header"
})}
variant="delete" variant="delete"
> >
<DialogContentText <DialogContentText>
dangerouslySetInnerHTML={{ <FormattedMessage
__html: i18n.t( defaultMessage="Are you sure you want to delete {counter, plural,
"Are you sure you want to remove <strong>{{ number }}</strong> products?", one {this product}
{ other {{displayQuantity} products}
number: maybe( }?"
() => params.ids.length.toString(), description="dialog content"
"..." values={{
) counter: maybe(() => params.ids.length),
} displayQuantity: (
) <strong>
}} {maybe(() => params.ids.length)}
/> </strong>
)
}}
/>
</DialogContentText>
</ActionDialog> </ActionDialog>
<ActionDialog <ActionDialog
open={params.action === "publish"} open={params.action === "publish"}
@ -303,21 +323,28 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
} }
}) })
} }
title={i18n.t("Publish products")} title={intl.formatMessage({
defaultMessage: "Publish Products",
description: "dialog header"
})}
> >
<DialogContentText <DialogContentText>
dangerouslySetInnerHTML={{ <FormattedMessage
__html: i18n.t( defaultMessage="Are you sure you want to publish {counter, plural,
"Are you sure you want to publish <strong>{{ number }}</strong> products?", one {this product}
{ other {{displayQuantity} products}
number: maybe( }?"
() => params.ids.length.toString(), description="dialog content"
"..." values={{
) counter: maybe(() => params.ids.length),
} displayQuantity: (
) <strong>
}} {maybe(() => params.ids.length)}
/> </strong>
)
}}
/>
</DialogContentText>
</ActionDialog> </ActionDialog>
<ActionDialog <ActionDialog
open={params.action === "unpublish"} open={params.action === "unpublish"}
@ -331,21 +358,28 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
} }
}) })
} }
title={i18n.t("Unpublish products")} title={intl.formatMessage({
defaultMessage: "Unpublish Products",
description: "dialog header"
})}
> >
<DialogContentText <DialogContentText>
dangerouslySetInnerHTML={{ <FormattedMessage
__html: i18n.t( defaultMessage="Are you sure you want to unpublish {counter, plural,
"Are you sure you want to unpublish <strong>{{ number }}</strong> products?", one {this product}
{ other {{displayQuantity} products}
number: maybe( }?"
() => params.ids.length.toString(), description="dialog content"
"..." values={{
) counter: maybe(() => params.ids.length),
} displayQuantity: (
) <strong>
}} {maybe(() => params.ids.length)}
/> </strong>
)
}}
/>
</DialogContentText>
</ActionDialog> </ActionDialog>
<SaveFilterTabDialog <SaveFilterTabDialog
open={params.action === "save-search"} open={params.action === "save-search"}

View file

@ -2,6 +2,7 @@ import DialogContentText from "@material-ui/core/DialogContentText";
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton";
import DeleteIcon from "@material-ui/icons/Delete"; import DeleteIcon from "@material-ui/icons/Delete";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import placeholderImg from "@assets/images/placeholder255x255.png"; import placeholderImg from "@assets/images/placeholder255x255.png";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
@ -9,10 +10,10 @@ import { WindowTitle } from "@saleor/components/WindowTitle";
import useBulkActions from "@saleor/hooks/useBulkActions"; import useBulkActions from "@saleor/hooks/useBulkActions";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl";
import { DEFAULT_INITIAL_SEARCH_DATA } from "../../../config"; import { DEFAULT_INITIAL_SEARCH_DATA } from "../../../config";
import SearchCategories from "../../../containers/SearchCategories"; import SearchCategories from "../../../containers/SearchCategories";
import SearchCollections from "../../../containers/SearchCollections"; import SearchCollections from "../../../containers/SearchCollections";
import i18n from "../../../i18n";
import { getMutationState, maybe } from "../../../misc"; import { getMutationState, maybe } from "../../../misc";
import { productTypeUrl } from "../../../productTypes/urls"; import { productTypeUrl } from "../../../productTypes/urls";
import ProductUpdatePage from "../../components/ProductUpdatePage"; import ProductUpdatePage from "../../components/ProductUpdatePage";
@ -53,6 +54,7 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
params.ids params.ids
); );
const intl = useIntl();
const openModal = (action: ProductUrlDialog) => const openModal = (action: ProductUrlDialog) =>
navigate( navigate(
@ -73,12 +75,18 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
> >
{({ data, loading, refetch }) => { {({ data, loading, refetch }) => {
const handleDelete = () => { const handleDelete = () => {
notify({ text: i18n.t("Product removed") }); notify({
text: intl.formatMessage({
defaultMessage: "Product removed"
})
});
navigate(productListUrl()); navigate(productListUrl());
}; };
const handleUpdate = (data: ProductUpdateMutationResult) => { const handleUpdate = (data: ProductUpdateMutationResult) => {
if (data.productUpdate.errors.length === 0) { if (data.productUpdate.errors.length === 0) {
notify({ text: i18n.t("Saved changes") }); notify({
text: intl.formatMessage(commonMessages.savedChanges)
});
} else { } else {
const attributeError = data.productUpdate.errors.find( const attributeError = data.productUpdate.errors.find(
err => err.field === "attributes" err => err.field === "attributes"
@ -103,7 +111,7 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
}; };
const handleImageDeleteSuccess = () => const handleImageDeleteSuccess = () =>
notify({ notify({
text: i18n.t("Image successfully deleted") text: intl.formatMessage(commonMessages.savedChanges)
}); });
const handleVariantAdd = () => const handleVariantAdd = () =>
navigate(productVariantAddUrl(id)); navigate(productVariantAddUrl(id));
@ -276,18 +284,20 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
confirmButtonState={deleteTransitionState} confirmButtonState={deleteTransitionState}
onConfirm={() => deleteProduct.mutate({ id })} onConfirm={() => deleteProduct.mutate({ id })}
variant="delete" variant="delete"
title={i18n.t("Remove product")} title={intl.formatMessage({
defaultMessage: "Delete Product",
description: "dialog header"
})}
> >
<DialogContentText <DialogContentText>
dangerouslySetInnerHTML={{ <FormattedMessage
__html: i18n.t( defaultMessage="Are you sure you want to delete {name}?"
"Are you sure you want to remove <strong>{{ name }}</strong>?", description="delete product"
{ values={{
name: product ? product.name : undefined name: product ? product.name : undefined
} }}
) />
}} </DialogContentText>
/>
</ActionDialog> </ActionDialog>
<ActionDialog <ActionDialog
open={params.action === "remove-variants"} open={params.action === "remove-variants"}
@ -301,21 +311,28 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
}) })
} }
variant="delete" variant="delete"
title={i18n.t("Remove product variants")} title={intl.formatMessage({
defaultMessage: "Delete Product Variants",
description: "dialog header"
})}
> >
<DialogContentText <DialogContentText>
dangerouslySetInnerHTML={{ <FormattedMessage
__html: i18n.t( defaultMessage="Are you sure you want to delete {counter, plural,
"Are you sure you want to remove <strong>{{ number }}</strong> variants?", one {this variant}
{ other {{displayQuantity} variants}
number: maybe( }?"
() => params.ids.length.toString(), description="dialog content"
"..." values={{
) counter: maybe(() => params.ids.length),
} displayQuantity: (
) <strong>
}} {maybe(() => params.ids.length)}
/> </strong>
)
}}
/>
</DialogContentText>
</ActionDialog> </ActionDialog>
</> </>
); );

View file

@ -1,10 +1,11 @@
import React from "react"; import React from "react";
import { useIntl } from "react-intl";
import placeholderImg from "@assets/images/placeholder255x255.png"; import placeholderImg from "@assets/images/placeholder255x255.png";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import i18n from "../../i18n"; import { commonMessages } from "@saleor/intl";
import { decimal, getMutationState, maybe } from "../../misc"; import { decimal, getMutationState, maybe } from "../../misc";
import ProductVariantDeleteDialog from "../components/ProductVariantDeleteDialog"; import ProductVariantDeleteDialog from "../components/ProductVariantDeleteDialog";
import ProductVariantPage, { import ProductVariantPage, {
@ -33,6 +34,7 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
}) => { }) => {
const navigate = useNavigator(); const navigate = useNavigator();
const notify = useNotifier(); const notify = useNotifier();
const intl = useIntl();
return ( return (
<TypedProductVariantQuery <TypedProductVariantQuery
@ -44,12 +46,16 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
const variant = data ? data.productVariant : undefined; const variant = data ? data.productVariant : undefined;
const handleBack = () => navigate(productUrl(productId)); const handleBack = () => navigate(productUrl(productId));
const handleDelete = () => { const handleDelete = () => {
notify({ text: i18n.t("Variant removed") }); notify({
text: intl.formatMessage({
defaultMessage: "Variant removed"
})
});
navigate(productUrl(productId)); navigate(productUrl(productId));
}; };
const handleUpdate = (data: VariantUpdate) => { const handleUpdate = (data: VariantUpdate) => {
if (!maybe(() => data.productVariantUpdate.errors.length)) { if (!maybe(() => data.productVariantUpdate.errors.length)) {
notify({ text: i18n.t("Changes saved") }); notify({ text: intl.formatMessage(commonMessages.savedChanges) });
} }
}; };

View file

@ -1,10 +1,10 @@
import React from "react"; import React from "react";
import { useIntl } from "react-intl";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import useShop from "@saleor/hooks/useShop"; import useShop from "@saleor/hooks/useShop";
import i18n from "../../i18n";
import { decimal, getMutationState, maybe } from "../../misc"; import { decimal, getMutationState, maybe } from "../../misc";
import ProductVariantCreatePage, { import ProductVariantCreatePage, {
ProductVariantCreatePageSubmitData ProductVariantCreatePageSubmitData
@ -24,6 +24,7 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
const navigate = useNavigator(); const navigate = useNavigator();
const notify = useNotifier(); const notify = useNotifier();
const shop = useShop(); const shop = useShop();
const intl = useIntl();
return ( return (
<TypedProductVariantCreateQuery <TypedProductVariantCreateQuery
@ -34,7 +35,11 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
{({ data, loading: productLoading }) => { {({ data, loading: productLoading }) => {
const handleCreateSuccess = (data: VariantCreate) => { const handleCreateSuccess = (data: VariantCreate) => {
if (data.productVariantCreate.errors.length === 0) { if (data.productVariantCreate.errors.length === 0) {
notify({ text: i18n.t("Product created") }); notify({
text: intl.formatMessage({
defaultMessage: "Product created"
})
});
navigate( navigate(
productVariantEditUrl( productVariantEditUrl(
productId, productId,
@ -81,7 +86,12 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
); );
return ( return (
<> <>
<WindowTitle title={i18n.t("Create variant")} /> <WindowTitle
title={intl.formatMessage({
defaultMessage: "Create Variant",
description: "window title"
})}
/>
<ProductVariantCreatePage <ProductVariantCreatePage
currencySymbol={maybe(() => shop.defaultCurrency)} currencySymbol={maybe(() => shop.defaultCurrency)}
errors={maybe( errors={maybe(
@ -89,7 +99,10 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
variantCreateResult.data.productVariantCreate.errors, variantCreateResult.data.productVariantCreate.errors,
[] []
)} )}
header={i18n.t("Add Variant")} header={intl.formatMessage({
defaultMessage: "Add Variant",
description: "header"
})}
loading={disableForm} loading={disableForm}
product={maybe(() => data.product)} product={maybe(() => data.product)}
onBack={handleBack} onBack={handleBack}