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 { schema {
query: Query query: Query
mutation: Mutations mutation: Mutation
} }
type AccountAddressCreate { type AccountAddressCreate {
@ -405,7 +405,6 @@ type AttributeValueDelete {
input AttributeValueInput { input AttributeValueInput {
id: ID id: ID
slug: String
values: [String]! values: [String]!
} }
@ -1990,7 +1989,7 @@ input MoveProductInput {
sortOrder: Int sortOrder: Int
} }
type Mutations { type Mutation {
webhookCreate(input: WebhookCreateInput!): WebhookCreate webhookCreate(input: WebhookCreateInput!): WebhookCreate
webhookDelete(id: ID!): WebhookDelete webhookDelete(id: ID!): WebhookDelete
webhookUpdate(id: ID!, input: WebhookUpdateInput!): WebhookUpdate webhookUpdate(id: ID!, input: WebhookUpdateInput!): WebhookUpdate
@ -3383,6 +3382,8 @@ type Query {
serviceAccount(id: ID!): ServiceAccount serviceAccount(id: ID!): ServiceAccount
user(id: ID!): User user(id: ID!): User
node(id: ID!): Node node(id: ID!): Node
_entities(representations: [_Any]): [_Entity]
_service: _Service
} }
type ReducedRate { type ReducedRate {
@ -4324,6 +4325,7 @@ enum WebhookEventTypeEnum {
ORDER_FULLY_PAID ORDER_FULLY_PAID
ORDER_UPDATED ORDER_UPDATED
ORDER_CANCELLED ORDER_CANCELLED
ORDER_FULFILLED
CUSTOMER_CREATED CUSTOMER_CREATED
PRODUCT_CREATED PRODUCT_CREATED
} }
@ -4361,3 +4363,11 @@ enum WeightUnitsEnum {
OZ OZ
G 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 Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent"; import CardContent from "@material-ui/core/CardContent";
import Typography from "@material-ui/core/Typography";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { IntlShape, useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import FormSpacer from "@saleor/components/FormSpacer";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import SingleAutocompleteSelectField, { import SingleAutocompleteSelectField, {
SingleAutocompleteChoiceType SingleAutocompleteChoiceType
@ -11,6 +13,8 @@ import SingleAutocompleteSelectField, {
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset"; import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset";
import { commonMessages } from "@saleor/intl"; 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"; import { ProductVariant_attributes_attribute_values } from "../../types/ProductVariant";
export interface VariantAttributeInputData { export interface VariantAttributeInputData {
@ -24,7 +28,7 @@ export type VariantAttributeInput = FormsetAtomicData<
interface ProductVariantAttributesProps { interface ProductVariantAttributesProps {
attributes: VariantAttributeInput[]; attributes: VariantAttributeInput[];
disabled: boolean; disabled: boolean;
errors: Record<string, string>; errors: VariantCreate_productVariantCreate_productErrors[];
onChange: FormsetChange<VariantAttributeInputData>; 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> = ({ const ProductVariantAttributes: React.FC<ProductVariantAttributesProps> = ({
attributes, attributes,
disabled, disabled,
@ -71,6 +88,8 @@ const ProductVariantAttributes: React.FC<ProductVariantAttributesProps> = ({
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const translatedErrors = translateErrors(intl);
return ( return (
<Card> <Card>
<CardTitle <CardTitle
@ -81,30 +100,38 @@ const ProductVariantAttributes: React.FC<ProductVariantAttributesProps> = ({
{attributes === undefined ? ( {attributes === undefined ? (
<Skeleton /> <Skeleton />
) : ( ) : (
attributes.map((attribute, attributeIndex) => { attributes.map(attribute => (
return ( <SingleAutocompleteSelectField
<SingleAutocompleteSelectField key={attribute.id}
key={attributeIndex} disabled={disabled}
disabled={disabled} displayValue={getAttributeDisplayValue(
displayValue={getAttributeDisplayValue( attribute.id,
attribute.id, attribute.value,
attribute.value, attributes
attributes )}
)} label={attribute.label}
error={!!errors[attribute.id]} name={`attribute:${attribute.id}`}
helperText={errors[attribute.id]} onChange={event => onChange(attribute.id, event.target.value)}
label={attribute.label} value={getAttributeValue(attribute.id, attributes)}
name={`attribute:${attribute.id}`} choices={getAttributeValueChoices(attribute.id, attributes)}
onChange={event => onChange(attribute.id, event.target.value)} allowCustomValues
value={getAttributeValue(attribute.id, attributes)} data-tc="variant-attribute-input"
choices={getAttributeValueChoices(attribute.id, attributes)} />
allowCustomValues ))
data-tc="variant-attribute-input"
/>
);
})
)} )}
</Grid> </Grid>
{errors.length > 0 && (
<>
<FormSpacer />
{errors
.filter(error => error.field === "attributes")
.map(error => (
<Typography color="error" key={error.code}>
{translatedErrors[error.code]}
</Typography>
))}
</>
)}
</CardContent> </CardContent>
</Card> </Card>
); );

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,17 +9,10 @@ import {
ProductDetails_product_collections, ProductDetails_product_collections,
ProductDetails_product_variants ProductDetails_product_variants
} from "@saleor/products/types/ProductDetails"; } from "@saleor/products/types/ProductDetails";
import { UserError } from "@saleor/types";
import { ProductAttributeInput } from "../components/ProductAttributes"; import { ProductAttributeInput } from "../components/ProductAttributes";
import { VariantAttributeInput } from "../components/ProductVariantAttributes"; import { VariantAttributeInput } from "../components/ProductVariantAttributes";
import { import { ProductVariant } from "../types/ProductVariant";
ProductVariant, import { ProductVariantCreateData_product } from "../types/ProductVariantCreateData";
ProductVariant_attributes_attribute
} from "../types/ProductVariant";
import {
ProductVariantCreateData_product,
ProductVariantCreateData_product_productType_variantAttributes
} from "../types/ProductVariantCreateData";
export interface Collection { export interface Collection {
id: string; 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)); navigate(productUrl(productId));
}; };
const handleUpdate = (data: VariantUpdate) => { const handleUpdate = (data: VariantUpdate) => {
if (!maybe(() => data.productVariantUpdate.errors.length)) { if (!maybe(() => data.productVariantUpdate.productErrors.length)) {
notify({ text: intl.formatMessage(commonMessages.savedChanges) }); notify({ text: intl.formatMessage(commonMessages.savedChanges) });
} }
}; };
@ -74,7 +74,10 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
const formTransitionState = getMutationState( const formTransitionState = getMutationState(
updateVariant.opts.called, updateVariant.opts.called,
updateVariant.opts.loading, updateVariant.opts.loading,
maybe(() => updateVariant.opts.data.productVariantUpdate.errors) maybe(
() =>
updateVariant.opts.data.productVariantUpdate.productErrors
)
); );
const removeTransitionState = getMutationState( const removeTransitionState = getMutationState(
deleteVariant.opts.called, deleteVariant.opts.called,
@ -105,7 +108,9 @@ export const ProductVariant: React.StatelessComponent<ProductUpdateProps> = ({
<WindowTitle title={maybe(() => data.productVariant.name)} /> <WindowTitle title={maybe(() => data.productVariant.name)} />
<ProductVariantPage <ProductVariantPage
errors={maybe( errors={maybe(
() => updateVariant.opts.data.productVariantUpdate.errors, () =>
updateVariant.opts.data.productVariantUpdate
.productErrors,
[] []
)} )}
saveButtonBarState={formTransitionState} saveButtonBarState={formTransitionState}

View file

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

View file

@ -1,4 +1,10 @@
export const formError = (field: string) => ({ export function formError<TErrorPayload>(
field, field: string,
message: "Generic form error" 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 React from "react";
import placeholderImage from "@assets/images/placeholder255x255.png"; 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 ProductVariantCreatePage from "../../../products/components/ProductVariantCreatePage";
import { product as productFixture } from "../../../products/fixtures"; import { product as productFixture } from "../../../products/fixtures";
import Decorator from "../../Decorator"; import Decorator from "../../Decorator";
@ -27,7 +27,24 @@ storiesOf("Views / Products / Create product variant", module)
.add("with errors", () => ( .add("with errors", () => (
<ProductVariantCreatePage <ProductVariantCreatePage
currencySymbol="USD" 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" header="Add variant"
loading={false} loading={false}
product={product} product={product}

View file

@ -2,7 +2,7 @@ import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
import placeholderImage from "@assets/images/placeholder60x60.png"; 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 ProductVariantPage from "../../../products/components/ProductVariantPage";
import { variant as variantFixture } from "../../../products/fixtures"; import { variant as variantFixture } from "../../../products/fixtures";
import Decorator from "../../Decorator"; import Decorator from "../../Decorator";
@ -51,6 +51,23 @@ storiesOf("Views / Products / Product variant details", module)
onSubmit={() => undefined} onSubmit={() => undefined}
onVariantClick={() => undefined} onVariantClick={() => undefined}
saveButtonBarState="default" 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 { export interface AttributeValueInput {
id?: string | null; id?: string | null;
slug?: string | null;
values: (string | null)[]; values: (string | null)[];
} }