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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
import React from "react";
import { useIntl } from "react-intl";
import { FieldType, IFilter } from "@saleor/components/Filter";
import FilterBar from "@saleor/components/FilterBar";
import i18n from "@saleor/i18n";
import { FilterProps } from "@saleor/types";
import { StockAvailability } from "@saleor/types/globalTypes";
import { ProductListUrlFilters } from "../../urls";
@ -17,79 +17,121 @@ export enum ProductFilterKeys {
stock,
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 => (
<FilterBar {...props} filterMenu={filterMenu} />
);
const ProductListFilter: React.FC<ProductListFilterProps> = props => {
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";
export default ProductListFilter;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,10 +9,10 @@ import {
} from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle";
import Skeleton from "@saleor/components/Skeleton";
import i18n from "../../../i18n";
import { ProductImage } from "../../types/ProductImage";
const styles = (theme: Theme) =>
@ -54,41 +54,51 @@ interface ProductVariantImagesProps extends WithStyles<typeof styles> {
export const ProductVariantImages = withStyles(styles, {
name: "ProductVariantImages"
})(({ classes, disabled, images, onImageAdd }: ProductVariantImagesProps) => (
<Card>
<CardTitle
title={i18n.t("Images")}
toolbar={
<Button
color="primary"
variant="text"
disabled={disabled}
onClick={onImageAdd}
>
{i18n.t("Choose photos")}
</Button>
}
/>
<CardContent>
<div className={classes.root}>
{images === undefined || images === null ? (
<Skeleton />
) : images.length > 0 ? (
images
.sort((prev, next) => (prev.sortOrder > next.sortOrder ? 1 : -1))
.map(tile => (
<div className={classes.imageContainer} key={tile.id}>
<img className={classes.image} src={tile.url} />
</div>
))
) : (
<Typography className={classes.helpText}>
{i18n.t("Select a specific variant image from product images")}
</Typography>
)}
</div>
</CardContent>
</Card>
));
})(({ classes, disabled, images, onImageAdd }: ProductVariantImagesProps) => {
const intl = useIntl();
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Images",
description: "section header"
})}
toolbar={
<Button
color="primary"
variant="text"
disabled={disabled}
onClick={onImageAdd}
>
<FormattedMessage
defaultMessage="Choose photos"
description="button"
/>
</Button>
}
/>
<CardContent>
<div className={classes.root}>
{images === undefined || images === null ? (
<Skeleton />
) : images.length > 0 ? (
images
.sort((prev, next) => (prev.sortOrder > next.sortOrder ? 1 : -1))
.map(tile => (
<div className={classes.imageContainer} key={tile.id}>
<img className={classes.image} src={tile.url} />
</div>
))
) : (
<Typography className={classes.helpText}>
<FormattedMessage defaultMessage="Select a specific variant image from product images" />
</Typography>
)}
</div>
</CardContent>
</Card>
);
});
ProductVariantImages.displayName = "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 classNames from "classnames";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle";
import Skeleton from "@saleor/components/Skeleton";
import TableCellAvatar from "@saleor/components/TableCellAvatar";
import i18n from "../../../i18n";
import { maybe, renderCollection } from "../../../misc";
import { ProductVariantCreateData_product_variants } from "../../types/ProductVariantCreateData";
import { ProductVariantDetails_productVariant } from "../../types/ProductVariantDetails";
@ -63,52 +63,70 @@ const ProductVariantNavigation = withStyles(styles, {
variants,
onAdd,
onRowClick
}: ProductVariantNavigationProps) => (
<Card>
<CardTitle title={i18n.t("Variants")} />
<Table>
<TableBody>
{renderCollection(variants, variant => (
<TableRow
hover={!!variant}
key={variant ? variant.id : "skeleton"}
className={classes.link}
onClick={variant ? () => onRowClick(variant.id) : undefined}
>
<TableCellAvatar
className={classNames({
[classes.tabActive]: variant && variant.id === current
})}
thumbnail={maybe(
() => variant.images[0].url,
fallbackThumbnail
)}
/>
<TableCell className={classes.textLeft}>
{variant ? variant.name || variant.sku : <Skeleton />}
</TableCell>
</TableRow>
))}
{onAdd ? (
<TableRow>
<TableCell colSpan={2}>
<Button color="primary" onClick={onAdd}>
{i18n.t("Add variant")}
</Button>
</TableCell>
</TableRow>
) : (
<TableRow>
<TableCellAvatar className={classes.tabActive} thumbnail={null} />
<TableCell className={classes.textLeft}>
{i18n.t("New Variant")}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</Card>
)
}: ProductVariantNavigationProps) => {
const intl = useIntl();
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Variants",
description: "section header"
})}
/>
<Table>
<TableBody>
{renderCollection(variants, variant => (
<TableRow
hover={!!variant}
key={variant ? variant.id : "skeleton"}
className={classes.link}
onClick={variant ? () => onRowClick(variant.id) : undefined}
>
<TableCellAvatar
className={classNames({
[classes.tabActive]: variant && variant.id === current
})}
thumbnail={maybe(
() => variant.images[0].url,
fallbackThumbnail
)}
/>
<TableCell className={classes.textLeft}>
{variant ? variant.name || variant.sku : <Skeleton />}
</TableCell>
</TableRow>
))}
{onAdd ? (
<TableRow>
<TableCell colSpan={2}>
<Button color="primary" onClick={onAdd}>
<FormattedMessage
defaultMessage="Add variant"
description="button"
/>
</Button>
</TableCell>
</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";
export default ProductVariantNavigation;

View file

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

View file

@ -8,9 +8,9 @@ import {
} from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import React from "react";
import { useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle";
import i18n from "../../../i18n";
const styles = (theme: Theme) =>
createStyles({
@ -42,48 +42,67 @@ const ProductVariantStock = withStyles(styles, { name: "ProductVariantStock" })(
stockAllocated,
loading,
onChange
}: ProductVariantStockProps) => (
<Card>
<CardTitle title={i18n.t("Stock")} />
<CardContent>
<div className={classes.grid}>
<div>
<TextField
error={!!errors.quantity}
name="quantity"
value={quantity}
label={i18n.t("Inventory")}
helperText={
errors.quantity
? errors.quantity
: !!stockAllocated
? i18n.t("Allocated: {{ quantity }}", {
context: "variant allocated stock",
quantity: stockAllocated
})
: undefined
}
onChange={onChange}
disabled={loading}
fullWidth
/>
}: ProductVariantStockProps) => {
const intl = useIntl();
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Stock",
description: "product variant stock, section header"
})}
/>
<CardContent>
<div className={classes.grid}>
<div>
<TextField
error={!!errors.quantity}
name="quantity"
value={quantity}
label={intl.formatMessage({
defaultMessage: "Inventory",
description: "product variant stock"
})}
helperText={
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>
<TextField
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>
)
</CardContent>
</Card>
);
}
);
ProductVariantStock.displayName = "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 Typography from "@material-ui/core/Typography";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle";
import Checkbox from "@saleor/components/Checkbox";
@ -21,7 +22,6 @@ import Money from "@saleor/components/Money";
import Skeleton from "@saleor/components/Skeleton";
import StatusLabel from "@saleor/components/StatusLabel";
import TableHead from "@saleor/components/TableHead";
import i18n from "../../../i18n";
import { renderCollection } from "../../../misc";
import { ListActions } from "../../../types";
import { ProductDetails_product_variants } from "../../types/ProductDetails";
@ -88,130 +88,162 @@ export const ProductVariants = withStyles(styles, { name: "ProductVariants" })(
toggle,
toggleAll,
toolbar
}: ProductVariantsProps) => (
<Card>
<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;
}: ProductVariantsProps) => {
const intl = useIntl();
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>
<TableCell className={classes.colName}>
{variant ? variant.name || variant.sku : <Skeleton />}
</TableCell>
<TableCell className={classes.colStatus}>
{variant ? (
<StatusLabel
status={variant.stockQuantity > 0 ? "success" : "error"}
label={
variant.stockQuantity > 0
? i18n.t("Available")
: i18n.t("Unavailable")
}
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Variants",
description: "section header"
})}
toolbar={
<>
<Button
onClick={onAttributesEdit}
variant="text"
color="primary"
data-tc="button-edit-attributes"
>
<FormattedMessage
defaultMessage="Edit attributes"
description="product variant attributes, button"
/>
</Button>
<Button
onClick={onVariantAdd}
variant="text"
color="primary"
data-tc="button-add-variant"
>
<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)}
/>
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colSku}>
{variant ? variant.sku : <Skeleton />}
</TableCell>
<Hidden smDown>
<TableCell className={classes.colPrice}>
</TableCell>
<TableCell className={classes.colName}>
{variant ? variant.name || variant.sku : <Skeleton />}
</TableCell>
<TableCell className={classes.colStatus}>
{variant ? (
variant.priceOverride ? (
<Money money={variant.priceOverride} />
) : fallbackPrice ? (
<Money money={fallbackPrice} />
) : (
<Skeleton />
)
<StatusLabel
status={
variant.stockQuantity > 0 ? "success" : "error"
}
label={
variant.stockQuantity > 0
? intl.formatMessage({
defaultMessage: "Available",
description: "product variant status"
})
: intl.formatMessage({
defaultMessage: "Unavailable",
description: "product variant status"
})
}
/>
) : (
<Skeleton />
)}
</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>
<TableCell colSpan={numberOfColumns}>
{i18n.t("This product has no variants")}
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
</Card>
)
)
)}
</TableBody>
</Table>
</Card>
);
}
);
ProductVariants.displayName = "ProductVariants";
export default ProductVariants;

View file

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

View file

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

View file

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

View file

@ -3,6 +3,7 @@ import DialogContentText from "@material-ui/core/DialogContentText";
import IconButton from "@material-ui/core/IconButton";
import DeleteIcon from "@material-ui/icons/Delete";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import ActionDialog from "@saleor/components/ActionDialog";
import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog";
@ -19,7 +20,6 @@ import usePaginator, {
createPaginationState
} from "@saleor/hooks/usePaginator";
import useShop from "@saleor/hooks/useShop";
import i18n from "@saleor/i18n";
import { getMutationState, maybe } from "@saleor/misc";
import { ListViews } from "@saleor/types";
import ProductListPage from "../../components/ProductListPage";
@ -67,6 +67,8 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
const { updateListSettings, settings } = useListSettings<ProductListColumns>(
ListViews.PRODUCT_LIST
);
const intl = useIntl();
const tabs = getFilterTabs();
const currentTab =
@ -155,7 +157,9 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
if (data.productBulkDelete.errors.length === 0) {
closeModal();
notify({
text: i18n.t("Products removed")
text: intl.formatMessage({
defaultMessage: "Products removed"
})
});
reset();
refetch();
@ -166,7 +170,10 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
if (data.productBulkPublish.errors.length === 0) {
closeModal();
notify({
text: i18n.t("Changed publication status")
text: intl.formatMessage({
defaultMessage: "Changed publication status",
description: "product status update notification"
})
});
reset();
refetch();
@ -235,13 +242,19 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
openModal("unpublish", listElements)
}
>
{i18n.t("Unpublish")}
<FormattedMessage
defaultMessage="Unpublish"
description="unpublish product, button"
/>
</Button>
<Button
color="primary"
onClick={() => openModal("publish", listElements)}
>
{i18n.t("Publish")}
<FormattedMessage
defaultMessage="Publish"
description="publish product, button"
/>
</Button>
<IconButton
color="primary"
@ -274,22 +287,29 @@ export const ProductList: React.StatelessComponent<ProductListProps> = ({
variables: { ids: params.ids }
})
}
title={i18n.t("Remove products")}
title={intl.formatMessage({
defaultMessage: "Delete Products",
description: "dialog header"
})}
variant="delete"
>
<DialogContentText
dangerouslySetInnerHTML={{
__html: i18n.t(
"Are you sure you want to remove <strong>{{ number }}</strong> products?",
{
number: maybe(
() => params.ids.length.toString(),
"..."
)
}
)
}}
/>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {counter, plural,
one {this product}
other {{displayQuantity} products}
}?"
description="dialog content"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>
{maybe(() => params.ids.length)}
</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
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
dangerouslySetInnerHTML={{
__html: i18n.t(
"Are you sure you want to publish <strong>{{ number }}</strong> products?",
{
number: maybe(
() => params.ids.length.toString(),
"..."
)
}
)
}}
/>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to publish {counter, plural,
one {this product}
other {{displayQuantity} products}
}?"
description="dialog content"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>
{maybe(() => params.ids.length)}
</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
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
dangerouslySetInnerHTML={{
__html: i18n.t(
"Are you sure you want to unpublish <strong>{{ number }}</strong> products?",
{
number: maybe(
() => params.ids.length.toString(),
"..."
)
}
)
}}
/>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to unpublish {counter, plural,
one {this product}
other {{displayQuantity} products}
}?"
description="dialog content"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>
{maybe(() => params.ids.length)}
</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
<SaveFilterTabDialog
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 DeleteIcon from "@material-ui/icons/Delete";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import placeholderImg from "@assets/images/placeholder255x255.png";
import ActionDialog from "@saleor/components/ActionDialog";
@ -9,10 +10,10 @@ import { WindowTitle } from "@saleor/components/WindowTitle";
import useBulkActions from "@saleor/hooks/useBulkActions";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl";
import { DEFAULT_INITIAL_SEARCH_DATA } from "../../../config";
import SearchCategories from "../../../containers/SearchCategories";
import SearchCollections from "../../../containers/SearchCollections";
import i18n from "../../../i18n";
import { getMutationState, maybe } from "../../../misc";
import { productTypeUrl } from "../../../productTypes/urls";
import ProductUpdatePage from "../../components/ProductUpdatePage";
@ -53,6 +54,7 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
params.ids
);
const intl = useIntl();
const openModal = (action: ProductUrlDialog) =>
navigate(
@ -73,12 +75,18 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
>
{({ data, loading, refetch }) => {
const handleDelete = () => {
notify({ text: i18n.t("Product removed") });
notify({
text: intl.formatMessage({
defaultMessage: "Product removed"
})
});
navigate(productListUrl());
};
const handleUpdate = (data: ProductUpdateMutationResult) => {
if (data.productUpdate.errors.length === 0) {
notify({ text: i18n.t("Saved changes") });
notify({
text: intl.formatMessage(commonMessages.savedChanges)
});
} else {
const attributeError = data.productUpdate.errors.find(
err => err.field === "attributes"
@ -103,7 +111,7 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
};
const handleImageDeleteSuccess = () =>
notify({
text: i18n.t("Image successfully deleted")
text: intl.formatMessage(commonMessages.savedChanges)
});
const handleVariantAdd = () =>
navigate(productVariantAddUrl(id));
@ -276,18 +284,20 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
confirmButtonState={deleteTransitionState}
onConfirm={() => deleteProduct.mutate({ id })}
variant="delete"
title={i18n.t("Remove product")}
title={intl.formatMessage({
defaultMessage: "Delete Product",
description: "dialog header"
})}
>
<DialogContentText
dangerouslySetInnerHTML={{
__html: i18n.t(
"Are you sure you want to remove <strong>{{ name }}</strong>?",
{
name: product ? product.name : undefined
}
)
}}
/>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {name}?"
description="delete product"
values={{
name: product ? product.name : undefined
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
open={params.action === "remove-variants"}
@ -301,21 +311,28 @@ export const ProductUpdate: React.StatelessComponent<ProductUpdateProps> = ({
})
}
variant="delete"
title={i18n.t("Remove product variants")}
title={intl.formatMessage({
defaultMessage: "Delete Product Variants",
description: "dialog header"
})}
>
<DialogContentText
dangerouslySetInnerHTML={{
__html: i18n.t(
"Are you sure you want to remove <strong>{{ number }}</strong> variants?",
{
number: maybe(
() => params.ids.length.toString(),
"..."
)
}
)
}}
/>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {counter, plural,
one {this variant}
other {{displayQuantity} variants}
}?"
description="dialog content"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>
{maybe(() => params.ids.length)}
</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
</>
);

View file

@ -1,10 +1,11 @@
import React from "react";
import { useIntl } from "react-intl";
import placeholderImg from "@assets/images/placeholder255x255.png";
import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import i18n from "../../i18n";
import { commonMessages } from "@saleor/intl";
import { decimal, getMutationState, maybe } from "../../misc";
import ProductVariantDeleteDialog from "../components/ProductVariantDeleteDialog";
import ProductVariantPage, {
@ -33,6 +34,7 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
}) => {
const navigate = useNavigator();
const notify = useNotifier();
const intl = useIntl();
return (
<TypedProductVariantQuery
@ -44,12 +46,16 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
const variant = data ? data.productVariant : undefined;
const handleBack = () => navigate(productUrl(productId));
const handleDelete = () => {
notify({ text: i18n.t("Variant removed") });
notify({
text: intl.formatMessage({
defaultMessage: "Variant removed"
})
});
navigate(productUrl(productId));
};
const handleUpdate = (data: VariantUpdate) => {
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 { useIntl } from "react-intl";
import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import useShop from "@saleor/hooks/useShop";
import i18n from "../../i18n";
import { decimal, getMutationState, maybe } from "../../misc";
import ProductVariantCreatePage, {
ProductVariantCreatePageSubmitData
@ -24,6 +24,7 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
const navigate = useNavigator();
const notify = useNotifier();
const shop = useShop();
const intl = useIntl();
return (
<TypedProductVariantCreateQuery
@ -34,7 +35,11 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
{({ data, loading: productLoading }) => {
const handleCreateSuccess = (data: VariantCreate) => {
if (data.productVariantCreate.errors.length === 0) {
notify({ text: i18n.t("Product created") });
notify({
text: intl.formatMessage({
defaultMessage: "Product created"
})
});
navigate(
productVariantEditUrl(
productId,
@ -81,7 +86,12 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
);
return (
<>
<WindowTitle title={i18n.t("Create variant")} />
<WindowTitle
title={intl.formatMessage({
defaultMessage: "Create Variant",
description: "window title"
})}
/>
<ProductVariantCreatePage
currencySymbol={maybe(() => shop.defaultCurrency)}
errors={maybe(
@ -89,7 +99,10 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
variantCreateResult.data.productVariantCreate.errors,
[]
)}
header={i18n.t("Add Variant")}
header={intl.formatMessage({
defaultMessage: "Add Variant",
description: "header"
})}
loading={disableForm}
product={maybe(() => data.product)}
onBack={handleBack}