Add product variant translation view (#1285)

* Add product variant translation view

* Final adjustments
This commit is contained in:
Jakub Majorek 2021-08-10 16:38:48 +02:00 committed by GitHub
parent 6d3e346a19
commit b65a632e07
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 940 additions and 13 deletions

View file

@ -1583,6 +1583,10 @@
"context": "alert",
"string": "Channel limit reached"
},
"src_dot_channels_dot_pages_dot_ChannelsListPage_dot_3277722884": {
"context": "created channels counter",
"string": "{count}/{max} channels used"
},
"src_dot_channels_dot_pages_dot_ChannelsListPage_dot_3511613983": {
"context": "channel name",
"string": "Channel Name"
@ -3445,6 +3449,13 @@
"context": "orders section name",
"string": "Orders"
},
"src_dot_orders_dot_components_dot_2214147779": {
"context": "alert",
"string": "Order limit reached"
},
"src_dot_orders_dot_components_dot_3769643084": {
"string": "You have reached your order limit, you will be billed extra for orders above limit. If you would like to up your limit, contact your administration staff about raising your limits."
},
"src_dot_orders_dot_components_dot_DraftOrderChannelSectionCard_dot_1243773440": {
"context": "section header",
"string": "Sales channel"
@ -3670,6 +3681,10 @@
"context": "button",
"string": "Add products"
},
"src_dot_orders_dot_components_dot_OrderDraftListPage_dot_1629108523": {
"context": "placed orders counter",
"string": "{count}/{max} orders"
},
"src_dot_orders_dot_components_dot_OrderDraftListPage_dot_2826235371": {
"context": "button",
"string": "Create order"
@ -4010,9 +4025,9 @@
"context": "generate invoice button",
"string": "Generate"
},
"src_dot_orders_dot_components_dot_OrderListPage_dot_2214147779": {
"context": "alert",
"string": "Order limit reached"
"src_dot_orders_dot_components_dot_OrderListPage_dot_1629108523": {
"context": "placed order counter",
"string": "{count}/{max} orders"
},
"src_dot_orders_dot_components_dot_OrderListPage_dot_2225897825": {
"context": "button",
@ -4025,9 +4040,6 @@
"src_dot_orders_dot_components_dot_OrderListPage_dot_355376157": {
"string": "Search Orders..."
},
"src_dot_orders_dot_components_dot_OrderListPage_dot_3769643084": {
"string": "You have reached your order limit, you will be billed extra for orders above limit. If you would like to up your limit, contact your administration staff about raising your limits."
},
"src_dot_orders_dot_components_dot_OrderListPage_dot_875489544": {
"context": "tab name",
"string": "All Orders"
@ -5382,6 +5394,10 @@
"context": "alert",
"string": "SKU limit reached"
},
"src_dot_products_dot_components_dot_ProductListPage_dot_3155791658": {
"context": "created products counter",
"string": "{count}/{max} SKUs used"
},
"src_dot_products_dot_components_dot_ProductListPage_dot_3550330425": {
"string": "Search Products..."
},
@ -6508,6 +6524,10 @@
"context": "button",
"string": "Invite staff member"
},
"src_dot_staff_dot_components_dot_StaffListPage_dot_938945387": {
"context": "used staff users counter",
"string": "{count}/{max} members"
},
"src_dot_staff_dot_components_dot_StaffListPage_dot_active": {
"context": "staff member's account",
"string": "Active"
@ -6675,6 +6695,12 @@
"context": "translations section name",
"string": "Translations"
},
"src_dot_translations_dot_components_dot_ProductContextSwitcher_dot_1932340772": {
"string": "Translating"
},
"src_dot_translations_dot_components_dot_ProductContextSwitcher_dot_197468239": {
"string": "Main Product"
},
"src_dot_translations_dot_components_dot_TranslationFields_dot_1308081812": {
"string": "{numberOfFields} Translations, {numberOfTranslatedFields} Completed"
},
@ -6815,6 +6841,17 @@
"src_dot_translations_dot_components_dot_TranslationsPagesPage_dot_432157284": {
"string": "Page Title"
},
"src_dot_translations_dot_components_dot_TranslationsProductVariantsPage_dot_4165966072": {
"context": "attribute list",
"string": "Attribute {number}"
},
"src_dot_translations_dot_components_dot_TranslationsProductVariantsPage_dot_847507870": {
"context": "header",
"string": "Translation Product Variant \"{productName}\" - {languageCode}"
},
"src_dot_translations_dot_components_dot_TranslationsProductVariantsPage_dot_953327101": {
"string": "Variant Name"
},
"src_dot_translations_dot_components_dot_TranslationsProductsPage_dot_1406947243": {
"string": "Search Engine Description"
},
@ -7118,6 +7155,10 @@
"src_dot_warehouses_dot_components_dot_WarehouseInfo_dot_2622674857": {
"string": "Warehouse Name"
},
"src_dot_warehouses_dot_components_dot_WarehouseListPage_dot_1082745946": {
"context": "used warehouses counter",
"string": "{count}/{max} warehouses used"
},
"src_dot_warehouses_dot_components_dot_WarehouseListPage_dot_2304765290": {
"string": "Search Warehouse"
},

View file

@ -84,6 +84,41 @@ export const productTranslationFragment = gql`
}
}
`;
export const productVariantTranslationFragment = gql`
fragment ProductVariantTranslationFragment on ProductVariantTranslatableContent {
productVariant {
id
}
name
translation(languageCode: $language) {
id
name
language {
code
language
}
}
attributeValues {
id
name
richText
attributeValue {
id
}
translation(languageCode: $language) {
id
name
richText
language {
code
language
}
}
}
}
`;
export const saleTranslationFragment = gql`
fragment SaleTranslationFragment on SaleTranslatableContent {
sale {

View file

@ -0,0 +1,64 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { LanguageCodeEnum } from "./../../types/globalTypes";
// ====================================================
// GraphQL fragment: ProductVariantTranslationFragment
// ====================================================
export interface ProductVariantTranslationFragment_productVariant {
__typename: "ProductVariant";
id: string;
}
export interface ProductVariantTranslationFragment_translation_language {
__typename: "LanguageDisplay";
code: LanguageCodeEnum;
language: string;
}
export interface ProductVariantTranslationFragment_translation {
__typename: "ProductVariantTranslation";
id: string;
name: string;
language: ProductVariantTranslationFragment_translation_language;
}
export interface ProductVariantTranslationFragment_attributeValues_attributeValue {
__typename: "AttributeValue";
id: string;
}
export interface ProductVariantTranslationFragment_attributeValues_translation_language {
__typename: "LanguageDisplay";
code: LanguageCodeEnum;
language: string;
}
export interface ProductVariantTranslationFragment_attributeValues_translation {
__typename: "AttributeValueTranslation";
id: string;
name: string;
richText: any | null;
language: ProductVariantTranslationFragment_attributeValues_translation_language;
}
export interface ProductVariantTranslationFragment_attributeValues {
__typename: "AttributeValueTranslatableContent";
id: string;
name: string;
richText: any | null;
attributeValue: ProductVariantTranslationFragment_attributeValues_attributeValue | null;
translation: ProductVariantTranslationFragment_attributeValues_translation | null;
}
export interface ProductVariantTranslationFragment {
__typename: "ProductVariantTranslatableContent";
productVariant: ProductVariantTranslationFragment_productVariant | null;
name: string;
translation: ProductVariantTranslationFragment_translation | null;
attributeValues: ProductVariantTranslationFragment_attributeValues[];
}

View file

@ -0,0 +1,169 @@
import {
Card,
ClickAwayListener,
Grow,
MenuItem,
MenuList as Menu,
Paper,
Popper,
Typography
} from "@material-ui/core";
import ArrowDropDown from "@material-ui/icons/ArrowDropDown";
import useNavigator from "@saleor/hooks/useNavigator";
import { makeStyles } from "@saleor/macaw-ui";
import {
languageEntityUrl,
productVariantUrl,
TranslatableEntities
} from "@saleor/translations/urls";
import classNames from "classnames";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useProductVariantList } from "../../queries";
export interface ProductContextSwitcherProps {
productId: string;
selectedId: string;
languageCode: string;
}
const useStyles = makeStyles(
theme => ({
arrow: {
color: theme.palette.primary.main,
transition: theme.transitions.duration.standard + "ms"
},
container: {
display: "flex",
alignItems: "center",
paddingBottom: theme.spacing(1),
marginRight: theme.spacing(1)
},
label: {
paddingRight: theme.spacing(1)
},
menuContainer: {
cursor: "pointer",
display: "flex",
justifyContent: "space-between",
minWidth: 90,
padding: theme.spacing(),
position: "relative"
},
menuItem: {
textAlign: "justify"
},
menuPaper: {
maxHeight: `calc(100vh - ${theme.spacing(2)}px)`,
overflow: "scroll"
},
popover: {
zIndex: 1
},
rotate: {
transform: "rotate(180deg)"
}
}),
{ name: "ProductContextSwitcher" }
);
const ProductContextSwitcher: React.FC<ProductContextSwitcherProps> = ({
languageCode,
productId,
selectedId
}) => {
const classes = useStyles();
const navigate = useNavigator();
const intl = useIntl();
const { data } = useProductVariantList({
variables: { id: productId }
});
const [isExpanded, setExpandedState] = React.useState(false);
const anchor = React.useRef();
const items = [
{
label: intl.formatMessage({ defaultMessage: "Main Product" }),
value: productId,
onClick: () =>
navigate(
languageEntityUrl(
languageCode,
TranslatableEntities.products,
productId
)
)
},
...(data?.product?.variants?.map(({ name, sku, id }) => ({
label: name || sku,
value: id,
onClick: () => navigate(productVariantUrl(languageCode, productId, id))
})) || [])
];
return (
<div className={classes.container}>
<Typography className={classes.label}>
<FormattedMessage defaultMessage="Translating" />:
</Typography>
<div ref={anchor}>
<Card
className={classes.menuContainer}
onClick={() => setExpandedState(!isExpanded)}
>
<Typography>
{items.find(({ value }) => value === selectedId)?.label || "-"}
</Typography>
<ArrowDropDown
className={classNames(classes.arrow, {
[classes.rotate]: isExpanded
})}
/>
</Card>
<Popper
className={classes.popover}
open={isExpanded}
anchorEl={anchor.current}
transition
disablePortal
placement="bottom-end"
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === "bottom" ? "right top" : "right bottom"
}}
>
<Paper className={classes.menuPaper}>
<ClickAwayListener
onClickAway={() => setExpandedState(false)}
mouseEvent="onClick"
>
<Menu>
{items.map(({ label, value, onClick }) => (
<MenuItem
key={value}
className={classes.menuItem}
onClick={() => {
setExpandedState(false);
onClick();
}}
>
{label}
</MenuItem>
))}
</Menu>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
</div>
);
};
ProductContextSwitcher.displayName = "ProductContextSwitcher";
export default ProductContextSwitcher;

View file

@ -0,0 +1,2 @@
export * from "./ProductContextSwitcher";
export { default } from "./ProductContextSwitcher";

View file

@ -38,6 +38,7 @@ export interface TranslationFieldsProps {
initialState: boolean;
saveButtonState: ConfirmButtonTransitionState;
pagination?: Pagination;
richTextResetKey: string; // temporary workaround TODO: fix rich text editor
onEdit: (field: string) => void;
onDiscard: () => void;
onSubmit: (field: TranslationField, data: string | OutputData) => void;
@ -120,6 +121,7 @@ const TranslationFields: React.FC<TranslationFieldsProps> = props => {
title,
saveButtonState,
pagination,
richTextResetKey,
onEdit,
onDiscard,
onSubmit
@ -187,6 +189,7 @@ const TranslationFields: React.FC<TranslationFieldsProps> = props => {
/>
) : (
<TranslationFieldsRich
resetKey={richTextResetKey}
disabled={disabled}
edit={false}
initial={field.value}
@ -221,6 +224,7 @@ const TranslationFields: React.FC<TranslationFieldsProps> = props => {
/>
) : (
<TranslationFieldsRich
resetKey={richTextResetKey}
disabled={disabled}
edit={activeField === field.name}
initial={field.translation}

View file

@ -14,6 +14,7 @@ interface TranslationFieldsRichProps {
edit: boolean;
initial: string;
saveButtonState: ConfirmButtonTransitionState;
resetKey: string;
onDiscard: () => void;
onSubmit: (data: OutputData) => void;
}
@ -23,6 +24,7 @@ const TranslationFieldsRich: React.FC<TranslationFieldsRichProps> = ({
edit,
initial,
saveButtonState,
resetKey,
onDiscard,
onSubmit
}) => {
@ -59,7 +61,7 @@ const TranslationFieldsRich: React.FC<TranslationFieldsRichProps> = ({
</Typography>
) : (
<Typography>
<RichTextEditorContent data={JSON.parse(initial)} />
<RichTextEditorContent key={resetKey} data={JSON.parse(initial)} />
</Typography>
);
};

View file

@ -107,6 +107,7 @@ const TranslationsAttributesPage: React.FC<TranslationsAttributesPageProps> = ({
}
]}
saveButtonState={saveButtonState}
richTextResetKey={languageCode}
onEdit={onEdit}
onDiscard={onDiscard}
onSubmit={onSubmit}
@ -142,6 +143,7 @@ const TranslationsAttributesPage: React.FC<TranslationsAttributesPageProps> = ({
) || []
}
saveButtonState={saveButtonState}
richTextResetKey={languageCode}
pagination={{
settings,
onUpdateListSettings,

View file

@ -83,6 +83,7 @@ const TranslationsCategoriesPage: React.FC<TranslationsCategoriesPageProps> = ({
}
]}
saveButtonState={saveButtonState}
richTextResetKey={languageCode}
onEdit={onEdit}
onDiscard={onDiscard}
onSubmit={onSubmit}
@ -116,6 +117,7 @@ const TranslationsCategoriesPage: React.FC<TranslationsCategoriesPageProps> = ({
}
]}
saveButtonState={saveButtonState}
richTextResetKey={languageCode}
onEdit={onEdit}
onDiscard={onDiscard}
onSubmit={onSubmit}

View file

@ -84,6 +84,7 @@ const TranslationsCollectionsPage: React.FC<TranslationsCollectionsPageProps> =
}
]}
saveButtonState={saveButtonState}
richTextResetKey={languageCode}
onEdit={onEdit}
onDiscard={onDiscard}
onSubmit={onSubmit}
@ -117,6 +118,7 @@ const TranslationsCollectionsPage: React.FC<TranslationsCollectionsPageProps> =
}
]}
saveButtonState={saveButtonState}
richTextResetKey={languageCode}
onEdit={onEdit}
onDiscard={onDiscard}
onSubmit={onSubmit}

View file

@ -88,6 +88,7 @@ const TranslationsPagesPage: React.FC<TranslationsPagesPageProps> = ({
}
]}
saveButtonState={saveButtonState}
richTextResetKey={languageCode}
onEdit={onEdit}
onDiscard={onDiscard}
onSubmit={onSubmit}
@ -122,6 +123,7 @@ const TranslationsPagesPage: React.FC<TranslationsPagesPageProps> = ({
}
]}
saveButtonState={saveButtonState}
richTextResetKey={languageCode}
onEdit={onEdit}
onDiscard={onDiscard}
onSubmit={onSubmit}
@ -153,6 +155,7 @@ const TranslationsPagesPage: React.FC<TranslationsPagesPageProps> = ({
})) || []
}
saveButtonState={saveButtonState}
richTextResetKey={languageCode}
onEdit={onEdit}
onDiscard={onDiscard}
onSubmit={onAttributeValueSubmit}

View file

@ -0,0 +1,136 @@
import CardSpacer from "@saleor/components/CardSpacer";
import Container from "@saleor/components/Container";
import LanguageSwitch from "@saleor/components/LanguageSwitch";
import PageHeader from "@saleor/components/PageHeader";
import { ProductVariantTranslationFragment } from "@saleor/fragments/types/ProductVariantTranslationFragment";
import { commonMessages, sectionNames } from "@saleor/intl";
import { Backlink } from "@saleor/macaw-ui";
import { getStringOrPlaceholder } from "@saleor/misc";
import {
TranslationInputFieldName,
TranslationsEntitiesPageProps
} from "@saleor/translations/types";
import React from "react";
import { useIntl } from "react-intl";
import { LanguageCodeEnum } from "../../../types/globalTypes";
import ProductContextSwitcher from "../ProductContextSwitcher";
import TranslationFields from "../TranslationFields";
export interface TranslationsProductsPageProps
extends TranslationsEntitiesPageProps {
data: ProductVariantTranslationFragment;
productId: string;
variantId: string;
onAttributeValueSubmit: TranslationsEntitiesPageProps["onSubmit"];
}
const TranslationsProductsPage: React.FC<TranslationsProductsPageProps> = ({
activeField,
disabled,
languageCode,
languages,
data,
saveButtonState,
productId,
variantId,
onBack,
onDiscard,
onEdit,
onLanguageChange,
onSubmit,
onAttributeValueSubmit
}) => {
const intl = useIntl();
return (
<Container>
<Backlink onClick={onBack}>
{intl.formatMessage(sectionNames.products)}
</Backlink>
<PageHeader
title={intl.formatMessage(
{
defaultMessage:
'Translation Product Variant "{productName}" - {languageCode}',
description: "header"
},
{
languageCode,
productName: getStringOrPlaceholder(data?.name)
}
)}
>
<ProductContextSwitcher
languageCode={languageCode}
productId={productId}
selectedId={variantId}
/>
<LanguageSwitch
currentLanguage={LanguageCodeEnum[languageCode]}
languages={languages}
onLanguageChange={onLanguageChange}
/>
</PageHeader>
<TranslationFields
activeField={activeField}
disabled={disabled}
initialState={true}
title={intl.formatMessage(commonMessages.generalInformations)}
fields={[
{
displayName: intl.formatMessage({
defaultMessage: "Variant Name"
}),
name: TranslationInputFieldName.name,
translation: data?.translation?.name || null,
type: "short" as "short",
value: data?.name
}
]}
saveButtonState={saveButtonState}
richTextResetKey={languageCode}
onEdit={onEdit}
onDiscard={onDiscard}
onSubmit={onSubmit}
/>
<CardSpacer />
{data?.attributeValues?.length > 0 && (
<>
<TranslationFields
activeField={activeField}
disabled={disabled}
initialState={true}
title={intl.formatMessage(commonMessages.translationAttributes)}
fields={
data.attributeValues.map((attrVal, i) => ({
id: attrVal.attributeValue.id,
displayName: intl.formatMessage(
{
defaultMessage: "Attribute {number}",
description: "attribute list"
},
{
number: i + 1
}
),
name: attrVal?.name,
translation: attrVal?.translation?.richText || null,
type: "rich" as "rich",
value: attrVal?.richText
})) || []
}
saveButtonState={saveButtonState}
richTextResetKey={languageCode}
onEdit={onEdit}
onDiscard={onDiscard}
onSubmit={onAttributeValueSubmit}
/>
<CardSpacer />
</>
)}
</Container>
);
};
TranslationsProductsPage.displayName = "TranslationsProductsPage";
export default TranslationsProductsPage;

View file

@ -0,0 +1,2 @@
export { default } from "./TranslationsProductVariantsPage";
export * from "./TranslationsProductVariantsPage";

View file

@ -14,15 +14,18 @@ import React from "react";
import { useIntl } from "react-intl";
import { LanguageCodeEnum } from "../../../types/globalTypes";
import ProductContextSwitcher from "../ProductContextSwitcher";
import TranslationFields from "../TranslationFields";
export interface TranslationsProductsPageProps
extends TranslationsEntitiesPageProps {
data: ProductTranslationFragment;
productId: string;
onAttributeValueSubmit: TranslationsEntitiesPageProps["onSubmit"];
}
const TranslationsProductsPage: React.FC<TranslationsProductsPageProps> = ({
productId,
activeField,
disabled,
languageCode,
@ -56,6 +59,11 @@ const TranslationsProductsPage: React.FC<TranslationsProductsPageProps> = ({
}
)}
>
<ProductContextSwitcher
languageCode={languageCode}
productId={productId}
selectedId={productId}
/>
<LanguageSwitch
currentLanguage={LanguageCodeEnum[languageCode]}
languages={languages}
@ -88,6 +96,7 @@ const TranslationsProductsPage: React.FC<TranslationsProductsPageProps> = ({
}
]}
saveButtonState={saveButtonState}
richTextResetKey={languageCode}
onEdit={onEdit}
onDiscard={onDiscard}
onSubmit={onSubmit}
@ -121,6 +130,7 @@ const TranslationsProductsPage: React.FC<TranslationsProductsPageProps> = ({
}
]}
saveButtonState={saveButtonState}
richTextResetKey={languageCode}
onEdit={onEdit}
onDiscard={onDiscard}
onSubmit={onSubmit}
@ -152,6 +162,7 @@ const TranslationsProductsPage: React.FC<TranslationsProductsPageProps> = ({
})) || []
}
saveButtonState={saveButtonState}
richTextResetKey={languageCode}
onEdit={onEdit}
onDiscard={onDiscard}
onSubmit={onAttributeValueSubmit}

View file

@ -76,6 +76,7 @@ const TranslationsSalesPage: React.FC<TranslationsSalesPageProps> = ({
}
]}
saveButtonState={saveButtonState}
richTextResetKey={languageCode}
onEdit={onEdit}
onDiscard={onDiscard}
onSubmit={onSubmit}

View file

@ -87,6 +87,7 @@ const TranslationsShippingMethodPage: React.FC<TranslationsShippingMethodPagePro
}
]}
saveButtonState={saveButtonState}
richTextResetKey={languageCode}
onEdit={onEdit}
onDiscard={onDiscard}
onSubmit={onSubmit}

View file

@ -77,6 +77,7 @@ const TranslationsVouchersPage: React.FC<TranslationsVouchersPageProps> = ({
}
]}
saveButtonState={saveButtonState}
richTextResetKey={languageCode}
onEdit={onEdit}
onDiscard={onDiscard}
onSubmit={onSubmit}

View file

@ -29,6 +29,9 @@ import TranslationsPagesComponent, {
import TranslationsProductsComponent, {
TranslationsProductsQueryParams
} from "./views/TranslationsProducts";
import TranslationsProductVariantsComponent, {
TranslationsProductVariantsQueryParams
} from "./views/TranslationsProductVariants";
import TranslationsSaleComponent, {
TranslationsSalesQueryParams
} from "./views/TranslationsSales";
@ -107,6 +110,28 @@ const TranslationsProducts: React.FC<TranslationsEntityRouteProps> = ({
/>
);
};
type TranslationsProductVariantProps = RouteComponentProps<{
productId: string;
id: string;
languageCode: string;
}>;
const TranslationsProductVariants: React.FC<TranslationsProductVariantProps> = ({
location,
match
}) => {
const qs = parseQs(location.search.substr(1));
const params: TranslationsProductVariantsQueryParams = {
activeField: qs.activeField
};
return (
<TranslationsProductVariantsComponent
id={decodeURIComponent(match.params.id)}
productId={decodeURIComponent(match.params.productId)}
languageCode={LanguageCodeEnum[match.params.languageCode]}
params={params}
/>
);
};
const TranslationsSales: React.FC<TranslationsEntityRouteProps> = ({
location,
match
@ -214,6 +239,17 @@ const TranslationsRouter: React.FC = () => {
)}
component={TranslationsProducts}
/>
<Route
exact
path={languageEntityPath(
":languageCode",
TranslatableEntities.products,
":productId",
TranslatableEntities.productVariants,
":id"
)}
component={TranslationsProductVariants}
/>
<Route
exact
path={languageEntityPath(

View file

@ -26,6 +26,10 @@ import {
UpdateProductTranslations,
UpdateProductTranslationsVariables
} from "./types/UpdateProductTranslations";
import {
UpdateProductVariantTranslations,
UpdateProductVariantTranslationsVariables
} from "./types/UpdateProductVariantTranslations";
import {
UpdateSaleTranslations,
UpdateSaleTranslationsVariables
@ -76,6 +80,37 @@ export const TypedUpdateProductTranslations = TypedMutation<
UpdateProductTranslationsVariables
>(updateProductTranslations);
const updateProductVariantTranslations = gql`
mutation UpdateProductVariantTranslations(
$id: ID!
$input: NameTranslationInput!
$language: LanguageCodeEnum!
) {
productVariantTranslate(id: $id, input: $input, languageCode: $language) {
errors {
field
message
}
productVariant {
id
name
translation(languageCode: $language) {
id
name
language {
code
language
}
}
}
}
}
`;
export const TypedUpdateProductVariantTranslations = TypedMutation<
UpdateProductVariantTranslations,
UpdateProductVariantTranslationsVariables
>(updateProductVariantTranslations);
const updateCategoryTranslations = gql`
mutation UpdateCategoryTranslations(
$id: ID!

View file

@ -6,6 +6,7 @@ import {
collectionTranslationFragment,
pageTranslationFragment,
productTranslationFragment,
productVariantTranslationFragment,
saleTranslationFragment,
shippingMethodTranslationFragment,
voucherTranslationFragment
@ -54,6 +55,14 @@ import {
ProductTranslations,
ProductTranslationsVariables
} from "./types/ProductTranslations";
import {
ProductVariantList,
ProductVariantListVariables
} from "./types/ProductVariantList";
import {
ProductVariantTranslationDetails,
ProductVariantTranslationDetailsVariables
} from "./types/ProductVariantTranslationDetails";
import {
SaleTranslationDetails,
SaleTranslationDetailsVariables
@ -356,6 +365,39 @@ export const useProductTranslationDetails = makeQuery<
ProductTranslationDetailsVariables
>(productTranslationDetails);
const productVariantList = gql`
query ProductVariantList($id: ID!) {
product(id: $id) {
id
variants {
id
name
sku
}
}
}
`;
export const useProductVariantList = makeQuery<
ProductVariantList,
ProductVariantListVariables
>(productVariantList);
const productVariantTranslationDetails = gql`
${productVariantTranslationFragment}
query ProductVariantTranslationDetails(
$id: ID!
$language: LanguageCodeEnum!
) {
translation(kind: VARIANT, id: $id) {
...ProductVariantTranslationFragment
}
}
`;
export const useProductVariantTranslationDetails = makeQuery<
ProductVariantTranslationDetails,
ProductVariantTranslationDetailsVariables
>(productVariantTranslationDetails);
const categoryTranslationDetails = gql`
${categoryTranslationFragment}
query CategoryTranslationDetails($id: ID!, $language: LanguageCodeEnum!) {

View file

@ -0,0 +1,29 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: ProductVariantList
// ====================================================
export interface ProductVariantList_product_variants {
__typename: "ProductVariant";
id: string;
name: string;
sku: string;
}
export interface ProductVariantList_product {
__typename: "Product";
id: string;
variants: (ProductVariantList_product_variants | null)[] | null;
}
export interface ProductVariantList {
product: ProductVariantList_product | null;
}
export interface ProductVariantListVariables {
id: string;
}

View file

@ -0,0 +1,79 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { LanguageCodeEnum } from "./../../types/globalTypes";
// ====================================================
// GraphQL query operation: ProductVariantTranslationDetails
// ====================================================
export interface ProductVariantTranslationDetails_translation_ProductTranslatableContent {
__typename: "ProductTranslatableContent" | "CollectionTranslatableContent" | "CategoryTranslatableContent" | "AttributeTranslatableContent" | "AttributeValueTranslatableContent" | "PageTranslatableContent" | "ShippingMethodTranslatableContent" | "SaleTranslatableContent" | "VoucherTranslatableContent" | "MenuItemTranslatableContent";
}
export interface ProductVariantTranslationDetails_translation_ProductVariantTranslatableContent_productVariant {
__typename: "ProductVariant";
id: string;
}
export interface ProductVariantTranslationDetails_translation_ProductVariantTranslatableContent_translation_language {
__typename: "LanguageDisplay";
code: LanguageCodeEnum;
language: string;
}
export interface ProductVariantTranslationDetails_translation_ProductVariantTranslatableContent_translation {
__typename: "ProductVariantTranslation";
id: string;
name: string;
language: ProductVariantTranslationDetails_translation_ProductVariantTranslatableContent_translation_language;
}
export interface ProductVariantTranslationDetails_translation_ProductVariantTranslatableContent_attributeValues_attributeValue {
__typename: "AttributeValue";
id: string;
}
export interface ProductVariantTranslationDetails_translation_ProductVariantTranslatableContent_attributeValues_translation_language {
__typename: "LanguageDisplay";
code: LanguageCodeEnum;
language: string;
}
export interface ProductVariantTranslationDetails_translation_ProductVariantTranslatableContent_attributeValues_translation {
__typename: "AttributeValueTranslation";
id: string;
name: string;
richText: any | null;
language: ProductVariantTranslationDetails_translation_ProductVariantTranslatableContent_attributeValues_translation_language;
}
export interface ProductVariantTranslationDetails_translation_ProductVariantTranslatableContent_attributeValues {
__typename: "AttributeValueTranslatableContent";
id: string;
name: string;
richText: any | null;
attributeValue: ProductVariantTranslationDetails_translation_ProductVariantTranslatableContent_attributeValues_attributeValue | null;
translation: ProductVariantTranslationDetails_translation_ProductVariantTranslatableContent_attributeValues_translation | null;
}
export interface ProductVariantTranslationDetails_translation_ProductVariantTranslatableContent {
__typename: "ProductVariantTranslatableContent";
productVariant: ProductVariantTranslationDetails_translation_ProductVariantTranslatableContent_productVariant | null;
name: string;
translation: ProductVariantTranslationDetails_translation_ProductVariantTranslatableContent_translation | null;
attributeValues: ProductVariantTranslationDetails_translation_ProductVariantTranslatableContent_attributeValues[];
}
export type ProductVariantTranslationDetails_translation = ProductVariantTranslationDetails_translation_ProductTranslatableContent | ProductVariantTranslationDetails_translation_ProductVariantTranslatableContent;
export interface ProductVariantTranslationDetails {
translation: ProductVariantTranslationDetails_translation | null;
}
export interface ProductVariantTranslationDetailsVariables {
id: string;
language: LanguageCodeEnum;
}

View file

@ -0,0 +1,52 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { NameTranslationInput, LanguageCodeEnum } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: UpdateProductVariantTranslations
// ====================================================
export interface UpdateProductVariantTranslations_productVariantTranslate_errors {
__typename: "TranslationError";
field: string | null;
message: string | null;
}
export interface UpdateProductVariantTranslations_productVariantTranslate_productVariant_translation_language {
__typename: "LanguageDisplay";
code: LanguageCodeEnum;
language: string;
}
export interface UpdateProductVariantTranslations_productVariantTranslate_productVariant_translation {
__typename: "ProductVariantTranslation";
id: string;
name: string;
language: UpdateProductVariantTranslations_productVariantTranslate_productVariant_translation_language;
}
export interface UpdateProductVariantTranslations_productVariantTranslate_productVariant {
__typename: "ProductVariant";
id: string;
name: string;
translation: UpdateProductVariantTranslations_productVariantTranslate_productVariant_translation | null;
}
export interface UpdateProductVariantTranslations_productVariantTranslate {
__typename: "ProductVariantTranslate";
errors: UpdateProductVariantTranslations_productVariantTranslate_errors[];
productVariant: UpdateProductVariantTranslations_productVariantTranslate_productVariant | null;
}
export interface UpdateProductVariantTranslations {
productVariantTranslate: UpdateProductVariantTranslations_productVariantTranslate | null;
}
export interface UpdateProductVariantTranslationsVariables {
id: string;
input: NameTranslationInput;
language: LanguageCodeEnum;
}

View file

@ -7,6 +7,7 @@ import { TranslationsEntitiesListFilterTab } from "./components/TranslationsEnti
export enum TranslatableEntities {
categories = "categories",
products = "products",
productVariants = "variants",
collections = "collections",
sales = "sales",
vouchers = "vouchers",
@ -35,10 +36,25 @@ export const languageEntitiesUrl = (
export const languageEntityPath = (
code: string,
entity: TranslatableEntities,
id: string
) => urlJoin(languageEntitiesPath(code), entity.toString(), id);
id: string,
...args: string[]
) => urlJoin(languageEntitiesPath(code), entity.toString(), id, ...args);
export const languageEntityUrl = (
code: string,
entity: TranslatableEntities,
id: string
) => languageEntityPath(code, entity, encodeURIComponent(id));
id: string,
...args: string[]
) => languageEntityPath(code, entity, encodeURIComponent(id), ...args);
export const productVariantUrl = (
code: string,
productId: string,
variantId: string
) =>
languageEntityUrl(
code,
TranslatableEntities.products,
productId,
TranslatableEntities.productVariants,
variantId
);

View file

@ -194,9 +194,12 @@ const TranslationsEntities: React.FC<TranslationsEntitiesProps> = ({
node.translation?.description,
node.translation?.name,
node.translation?.seoDescription,
node.translation?.seoTitle
node.translation?.seoTitle,
...(node.attributeValues?.map(
({ translation }) => translation?.richText
) || [])
]),
max: 4
max: 4 + (node.attributeValues?.length || 0)
},
id: node?.product?.id,
name: node?.product?.name

View file

@ -0,0 +1,156 @@
import { OutputData } from "@editorjs/editorjs";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import useShop from "@saleor/hooks/useShop";
import { commonMessages } from "@saleor/intl";
import { stringify as stringifyQs } from "qs";
import React from "react";
import { useIntl } from "react-intl";
import { maybe } from "../../misc";
import { LanguageCodeEnum } from "../../types/globalTypes";
import TranslationsProductVariantsPage from "../components/TranslationsProductVariantsPage";
import {
TypedUpdateAttributeValueTranslations,
TypedUpdateProductVariantTranslations
} from "../mutations";
import { useProductVariantTranslationDetails } from "../queries";
import { TranslationField, TranslationInputFieldName } from "../types";
import {
languageEntitiesUrl,
productVariantUrl,
TranslatableEntities
} from "../urls";
import { getParsedTranslationInputData } from "../utils";
export interface TranslationsProductVariantsQueryParams {
activeField: string;
}
export interface TranslationsProductVariantsProps {
id: string;
productId: string;
languageCode: LanguageCodeEnum;
params: TranslationsProductVariantsQueryParams;
}
const TranslationsProductVariants: React.FC<TranslationsProductVariantsProps> = ({
id,
productId,
languageCode,
params
}) => {
const navigate = useNavigator();
const notify = useNotifier();
const shop = useShop();
const intl = useIntl();
const productVariantTranslations = useProductVariantTranslationDetails({
variables: { id, language: languageCode }
});
const onEdit = (field: string) =>
navigate(
"?" +
stringifyQs({
activeField: field
}),
true
);
const onUpdate = (errors: unknown[]) => {
if (errors.length === 0) {
productVariantTranslations.refetch();
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
navigate("?", true);
}
};
const onDiscard = () => {
navigate("?", true);
};
return (
<TypedUpdateProductVariantTranslations
onCompleted={data => onUpdate(data.productVariantTranslate.errors)}
>
{(updateTranslations, updateTranslationsOpts) => (
<TypedUpdateAttributeValueTranslations
onCompleted={data => onUpdate(data.attributeValueTranslate.errors)}
>
{updateAttributeValueTranslations => {
const handleSubmit = (
{ name: fieldName }: TranslationField<TranslationInputFieldName>,
data: string
) => {
updateTranslations({
variables: {
id,
input: getParsedTranslationInputData({
data,
fieldName
}),
language: languageCode
}
});
};
const handleAttributeValueSubmit = (
{ id }: TranslationField<TranslationInputFieldName>,
data: OutputData
) => {
updateAttributeValueTranslations({
variables: {
id,
input: { richText: JSON.stringify(data) },
language: languageCode
}
});
};
const translation = productVariantTranslations?.data?.translation;
return (
<TranslationsProductVariantsPage
productId={productId}
variantId={id}
activeField={params.activeField}
disabled={
productVariantTranslations.loading ||
updateTranslationsOpts.loading
}
languageCode={languageCode}
languages={maybe(() => shop.languages, [])}
saveButtonState={updateTranslationsOpts.status}
onBack={() =>
navigate(
languageEntitiesUrl(languageCode, {
tab: TranslatableEntities.products
})
)
}
onEdit={onEdit}
onDiscard={onDiscard}
onLanguageChange={lang =>
navigate(productVariantUrl(lang, productId, id))
}
onSubmit={handleSubmit}
onAttributeValueSubmit={handleAttributeValueSubmit}
data={
translation?.__typename ===
"ProductVariantTranslatableContent"
? translation
: null
}
/>
);
}}
</TypedUpdateAttributeValueTranslations>
)}
</TypedUpdateProductVariantTranslations>
);
};
TranslationsProductVariants.displayName = "TranslationsProductVariants";
export default TranslationsProductVariants;

View file

@ -112,6 +112,7 @@ const TranslationsProducts: React.FC<TranslationsProductsProps> = ({
return (
<TranslationsProductsPage
productId={id}
activeField={params.activeField}
disabled={
productTranslations.loading || updateTranslationsOpts.loading