Fix attribute errors

This commit is contained in:
dominik-zeglen 2019-10-17 17:29:13 +02:00
parent d63d9b41be
commit 4d27960d79
14 changed files with 153 additions and 114 deletions

View file

@ -1,6 +1,6 @@
schema {
query: Query
mutation: Mutations
mutation: Mutation
}
type AccountAddressCreate {
@ -405,7 +405,6 @@ type AttributeValueDelete {
input AttributeValueInput {
id: ID
slug: String
values: [String]!
}
@ -1990,7 +1989,7 @@ input MoveProductInput {
sortOrder: Int
}
type Mutations {
type Mutation {
webhookCreate(input: WebhookCreateInput!): WebhookCreate
webhookDelete(id: ID!): WebhookDelete
webhookUpdate(id: ID!, input: WebhookUpdateInput!): WebhookUpdate
@ -3383,6 +3382,8 @@ type Query {
serviceAccount(id: ID!): ServiceAccount
user(id: ID!): User
node(id: ID!): Node
_entities(representations: [_Any]): [_Entity]
_service: _Service
}
type ReducedRate {
@ -4324,6 +4325,7 @@ enum WebhookEventTypeEnum {
ORDER_FULLY_PAID
ORDER_UPDATED
ORDER_CANCELLED
ORDER_FULFILLED
CUSTOMER_CREATED
PRODUCT_CREATED
}
@ -4361,3 +4363,11 @@ enum WeightUnitsEnum {
OZ
G
}
scalar _Any
union _Entity = Address | ServiceAccount | User | ProductVariant | Product | ProductType | Collection | Category | ProductImage
type _Service {
sdl: String
}

View file

@ -1,9 +1,11 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import Typography from "@material-ui/core/Typography";
import React from "react";
import { useIntl } from "react-intl";
import { IntlShape, useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle";
import FormSpacer from "@saleor/components/FormSpacer";
import Grid from "@saleor/components/Grid";
import SingleAutocompleteSelectField, {
SingleAutocompleteChoiceType
@ -11,6 +13,8 @@ import SingleAutocompleteSelectField, {
import Skeleton from "@saleor/components/Skeleton";
import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset";
import { commonMessages } from "@saleor/intl";
import { VariantCreate_productVariantCreate_productErrors } from "@saleor/products/types/VariantCreate";
import { ProductErrorCode } from "@saleor/types/globalTypes";
import { ProductVariant_attributes_attribute_values } from "../../types/ProductVariant";
export interface VariantAttributeInputData {
@ -24,7 +28,7 @@ export type VariantAttributeInput = FormsetAtomicData<
interface ProductVariantAttributesProps {
attributes: VariantAttributeInput[];
disabled: boolean;
errors: Record<string, string>;
errors: VariantCreate_productVariantCreate_productErrors[];
onChange: FormsetChange<VariantAttributeInputData>;
}
@ -63,6 +67,19 @@ function getAttributeValueChoices(
}));
}
function translateErrors(intl: IntlShape) {
return {
[ProductErrorCode.REQUIRED]: intl.formatMessage({
defaultMessage: "All attributes should have value",
description: "product attribute error"
}),
[ProductErrorCode.UNIQUE]: intl.formatMessage({
defaultMessage: "This variant already exists",
description: "product attribute error"
})
};
}
const ProductVariantAttributes: React.FC<ProductVariantAttributesProps> = ({
attributes,
disabled,
@ -71,6 +88,8 @@ const ProductVariantAttributes: React.FC<ProductVariantAttributesProps> = ({
}) => {
const intl = useIntl();
const translatedErrors = translateErrors(intl);
return (
<Card>
<CardTitle
@ -81,30 +100,38 @@ const ProductVariantAttributes: React.FC<ProductVariantAttributesProps> = ({
{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"
/>
);
})
attributes.map(attribute => (
<SingleAutocompleteSelectField
key={attribute.id}
disabled={disabled}
displayValue={getAttributeDisplayValue(
attribute.id,
attribute.value,
attributes
)}
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>
{errors.length > 0 && (
<>
<FormSpacer />
{errors
.filter(error => error.field === "attributes")
.map(error => (
<Typography color="error" key={error.code}>
{translatedErrors[error.code]}
</Typography>
))}
</>
)}
</CardContent>
</Card>
);

View file

@ -13,12 +13,9 @@ import useFormset, {
FormsetChange,
FormsetData
} from "@saleor/hooks/useFormset";
import {
getVariantAttributeErrors,
getVariantAttributeInputFromProduct
} from "@saleor/products/utils/data";
import { VariantCreate_productVariantCreate_productErrors } from "@saleor/products/types/VariantCreate";
import { getVariantAttributeInputFromProduct } from "@saleor/products/utils/data";
import { maybe } from "../../../misc";
import { UserError } from "../../../types";
import { ProductVariantCreateData_product } from "../../types/ProductVariantCreateData";
import ProductVariantAttributes, {
VariantAttributeInputData
@ -42,7 +39,7 @@ export interface ProductVariantCreatePageSubmitData
interface ProductVariantCreatePageProps {
currencySymbol: string;
errors: UserError[];
errors: VariantCreate_productVariantCreate_productErrors[];
header: string;
loading: boolean;
product: ProductVariantCreateData_product;
@ -54,7 +51,7 @@ interface ProductVariantCreatePageProps {
const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
currencySymbol,
errors: formErrors,
errors: apiErrors,
loading,
header,
product,
@ -96,7 +93,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
});
return (
<Form initial={initialForm} errors={formErrors} onSubmit={handleSubmit}>
<Form initial={initialForm} errors={apiErrors} onSubmit={handleSubmit}>
{({ change, data, errors, hasChanged, submit, triggerChange }) => {
const handleAttributeChange: FormsetChange = (id, value) => {
changeAttributeData(id, value);
@ -123,10 +120,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
<ProductVariantAttributes
attributes={attributes}
disabled={loading}
errors={getVariantAttributeErrors(
formErrors,
maybe(() => product.productType.variantAttributes)
)}
errors={apiErrors}
onChange={handleAttributeChange}
/>
<CardSpacer />

View file

@ -12,12 +12,9 @@ import useFormset, {
FormsetChange,
FormsetData
} from "@saleor/hooks/useFormset";
import {
getAttributeInputFromVariant,
getVariantAttributeErrors
} from "@saleor/products/utils/data";
import { VariantUpdate_productVariantUpdate_productErrors } from "@saleor/products/types/VariantUpdate";
import { getAttributeInputFromVariant } from "@saleor/products/utils/data";
import { maybe } from "../../../misc";
import { UserError } from "../../../types";
import { ProductVariant } from "../../types/ProductVariant";
import ProductVariantAttributes, {
VariantAttributeInputData
@ -42,7 +39,7 @@ export interface ProductVariantPageSubmitData
interface ProductVariantPageProps {
variant?: ProductVariant;
errors: UserError[];
errors: VariantUpdate_productVariantUpdate_productErrors[];
saveButtonBarState: ConfirmButtonTransitionState;
loading?: boolean;
placeholderImage?: string;
@ -56,7 +53,7 @@ interface ProductVariantPageProps {
}
const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
errors: formErrors,
errors: apiErrors,
loading,
header,
placeholderImage,
@ -114,7 +111,7 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
<PageHeader title={header} />
<Form
initial={initialForm}
errors={formErrors}
errors={apiErrors}
onSubmit={handleSubmit}
confirmLeave
>
@ -146,14 +143,7 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
<ProductVariantAttributes
attributes={attributes}
disabled={loading}
errors={getVariantAttributeErrors(
formErrors,
maybe(() =>
variant.attributes.map(
attribute => attribute.attribute
)
)
)}
errors={apiErrors}
onChange={handleAttributeChange}
/>
<CardSpacer />

View file

@ -306,7 +306,8 @@ export const variantUpdateMutation = gql`
trackInventory: $trackInventory
}
) {
errors {
productErrors {
code
field
message
}
@ -325,7 +326,8 @@ export const variantCreateMutation = gql`
${fragmentVariant}
mutation VariantCreate($input: ProductVariantCreateInput!) {
productVariantCreate(input: $input) {
errors {
productErrors {
code
field
message
}

View file

@ -2,14 +2,15 @@
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ProductVariantCreateInput } from "./../../types/globalTypes";
import { ProductVariantCreateInput, ProductErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: VariantCreate
// ====================================================
export interface VariantCreate_productVariantCreate_errors {
__typename: "Error";
export interface VariantCreate_productVariantCreate_productErrors {
__typename: "ProductError";
code: ProductErrorCode | null;
field: string | null;
message: string | null;
}
@ -113,7 +114,7 @@ export interface VariantCreate_productVariantCreate_productVariant {
export interface VariantCreate_productVariantCreate {
__typename: "ProductVariantCreate";
errors: VariantCreate_productVariantCreate_errors[] | null;
productErrors: VariantCreate_productVariantCreate_productErrors[] | null;
productVariant: VariantCreate_productVariantCreate_productVariant | null;
}

View file

@ -2,14 +2,15 @@
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeValueInput } from "./../../types/globalTypes";
import { AttributeValueInput, ProductErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: VariantUpdate
// ====================================================
export interface VariantUpdate_productVariantUpdate_errors {
__typename: "Error";
export interface VariantUpdate_productVariantUpdate_productErrors {
__typename: "ProductError";
code: ProductErrorCode | null;
field: string | null;
message: string | null;
}
@ -113,7 +114,7 @@ export interface VariantUpdate_productVariantUpdate_productVariant {
export interface VariantUpdate_productVariantUpdate {
__typename: "ProductVariantUpdate";
errors: VariantUpdate_productVariantUpdate_errors[] | null;
productErrors: VariantUpdate_productVariantUpdate_productErrors[] | null;
productVariant: VariantUpdate_productVariantUpdate_productVariant | null;
}

View file

@ -9,17 +9,10 @@ import {
ProductDetails_product_collections,
ProductDetails_product_variants
} from "@saleor/products/types/ProductDetails";
import { UserError } from "@saleor/types";
import { ProductAttributeInput } from "../components/ProductAttributes";
import { VariantAttributeInput } from "../components/ProductVariantAttributes";
import {
ProductVariant,
ProductVariant_attributes_attribute
} from "../types/ProductVariant";
import {
ProductVariantCreateData_product,
ProductVariantCreateData_product_productType_variantAttributes
} from "../types/ProductVariantCreateData";
import { ProductVariant } from "../types/ProductVariant";
import { ProductVariantCreateData_product } from "../types/ProductVariantCreateData";
export interface Collection {
id: string;
@ -197,28 +190,3 @@ export function getProductUpdatePageFormData(
)
};
}
export function getVariantAttributeErrors(
errors: UserError[],
variantAttributes: Array<
| ProductVariantCreateData_product_productType_variantAttributes
| ProductVariant_attributes_attribute
>
): Record<string, string> {
return maybe(
() =>
errors.reduce((acc, err) => {
const slug = err.field.split(":")[1];
const attribute = variantAttributes.find(
attribute => attribute.slug === slug
);
if (!!attribute) {
acc[attribute.id] = err.message;
}
return acc;
}, {}),
{}
);
}

View file

@ -54,7 +54,7 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
navigate(productUrl(productId));
};
const handleUpdate = (data: VariantUpdate) => {
if (!maybe(() => data.productVariantUpdate.errors.length)) {
if (!maybe(() => data.productVariantUpdate.productErrors.length)) {
notify({ text: intl.formatMessage(commonMessages.savedChanges) });
}
};
@ -74,7 +74,10 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
const formTransitionState = getMutationState(
updateVariant.opts.called,
updateVariant.opts.loading,
maybe(() => updateVariant.opts.data.productVariantUpdate.errors)
maybe(
() =>
updateVariant.opts.data.productVariantUpdate.productErrors
)
);
const removeTransitionState = getMutationState(
deleteVariant.opts.called,
@ -105,7 +108,9 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
<WindowTitle title={maybe(() => data.productVariant.name)} />
<ProductVariantPage
errors={maybe(
() => updateVariant.opts.data.productVariantUpdate.errors,
() =>
updateVariant.opts.data.productVariantUpdate
.productErrors,
[]
)}
saveButtonBarState={formTransitionState}

View file

@ -34,7 +34,7 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
>
{({ data, loading: productLoading }) => {
const handleCreateSuccess = (data: VariantCreate) => {
if (data.productVariantCreate.errors.length === 0) {
if (data.productVariantCreate.productErrors.length === 0) {
notify({
text: intl.formatMessage({
defaultMessage: "Product created"
@ -83,7 +83,8 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
variantCreateResult.called,
variantCreateResult.loading,
maybe(
() => variantCreateResult.data.productVariantCreate.errors
() =>
variantCreateResult.data.productVariantCreate.productErrors
)
);
return (
@ -98,7 +99,8 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
currencySymbol={maybe(() => shop.defaultCurrency)}
errors={maybe(
() =>
variantCreateResult.data.productVariantCreate.errors,
variantCreateResult.data.productVariantCreate
.productErrors,
[]
)}
header={intl.formatMessage({

View file

@ -1,4 +1,10 @@
export const formError = (field: string) => ({
field,
message: "Generic form error"
});
export function formError<TErrorPayload>(
field: string,
opts?: Partial<Omit<TErrorPayload, "field" | "message">>
) {
return {
field,
message: "Generic form error",
...opts
};
}

View file

@ -2,7 +2,7 @@ import { storiesOf } from "@storybook/react";
import React from "react";
import placeholderImage from "@assets/images/placeholder255x255.png";
import { formError } from "@saleor/storybook/misc";
import { ProductErrorCode } from "@saleor/types/globalTypes";
import ProductVariantCreatePage from "../../../products/components/ProductVariantCreatePage";
import { product as productFixture } from "../../../products/fixtures";
import Decorator from "../../Decorator";
@ -27,7 +27,24 @@ storiesOf("Views / Products / Create product variant", module)
.add("with errors", () => (
<ProductVariantCreatePage
currencySymbol="USD"
errors={[formError("attributes:color")]}
errors={[
{
code: ProductErrorCode.REQUIRED,
field: "attributes"
},
{
code: ProductErrorCode.UNIQUE,
field: "attributes"
},
{
code: ProductErrorCode.ALREADY_EXISTS,
field: "sku"
}
].map(error => ({
__typename: "ProductError",
message: "Generic form error",
...error
}))}
header="Add variant"
loading={false}
product={product}

View file

@ -2,7 +2,7 @@ import { storiesOf } from "@storybook/react";
import React from "react";
import placeholderImage from "@assets/images/placeholder60x60.png";
import { formError } from "@saleor/storybook/misc";
import { ProductErrorCode } from "@saleor/types/globalTypes";
import ProductVariantPage from "../../../products/components/ProductVariantPage";
import { variant as variantFixture } from "../../../products/fixtures";
import Decorator from "../../Decorator";
@ -51,6 +51,23 @@ storiesOf("Views / Products / Product variant details", module)
onSubmit={() => undefined}
onVariantClick={() => undefined}
saveButtonBarState="default"
errors={["attributes:Borders", "attributes:Legacy"].map(formError)}
errors={[
{
code: ProductErrorCode.REQUIRED,
field: "attributes"
},
{
code: ProductErrorCode.UNIQUE,
field: "attributes"
},
{
code: ProductErrorCode.ALREADY_EXISTS,
field: "sku"
}
].map(error => ({
__typename: "ProductError",
message: "Generic form error",
...error
}))}
/>
));

View file

@ -382,7 +382,6 @@ export interface AttributeValueCreateInput {
export interface AttributeValueInput {
id?: string | null;
slug?: string | null;
values: (string | null)[];
}