diff --git a/src/components/AddressEdit/AddressEdit.tsx b/src/components/AddressEdit/AddressEdit.tsx index 15af2b550..8c0aa1d62 100644 --- a/src/components/AddressEdit/AddressEdit.tsx +++ b/src/components/AddressEdit/AddressEdit.tsx @@ -13,6 +13,7 @@ import FormSpacer from "../FormSpacer"; import SingleAutocompleteSelectField, { SingleAutocompleteChoiceType, } from "../SingleAutocompleteSelectField"; +import { useAddressValidation } from "./useAddressValidation"; const useStyles = makeStyles( theme => ({ @@ -46,6 +47,24 @@ function getErrorMessage( return getOrderErrorMessage(err, intl); } +const PossibleFormFields = { + CITY: "city", + CITY_AREA: "cityArea", + COUNTRY: "country", + COUNTRY_AREA: "countryArea", + FIRST_NAME: "firstName", + LAST_NAME: "lastName", + COMPANY_NAME: "companyName", + PHONE: "phone", + POSTAL_CODE: "postalCode", + STREET_ADDRESS_1: "streetAddress1", + STREET_ADDRESS_2: "streetAddress2", +} as const; + +const formFields: Array = Object.values( + PossibleFormFields, +); + const AddressEdit: React.FC = props => { const { countries, @@ -56,23 +75,9 @@ const AddressEdit: React.FC = props => { onChange, onCountryChange, } = props; - const classes = useStyles(props); const intl = useIntl(); - - const formFields: Array = [ - "city", - "cityArea", - "country", - "countryArea", - "firstName", - "lastName", - "companyName", - "phone", - "postalCode", - "streetAddress1", - "streetAddress2", - ]; + const { areas, isFieldAllowed } = useAddressValidation(data.country); const formErrors = getFormErrors< keyof AddressTypeInput, @@ -219,7 +224,6 @@ const AddressEdit: React.FC = props => { = props => {
= props => { />
- + {isFieldAllowed(PossibleFormFields.COUNTRY_AREA) && ( + + )}
diff --git a/src/components/AddressEdit/createCountryHandler.test.ts b/src/components/AddressEdit/createCountryHandler.test.ts new file mode 100644 index 000000000..5236ac251 --- /dev/null +++ b/src/components/AddressEdit/createCountryHandler.test.ts @@ -0,0 +1,20 @@ +import { ChangeEvent } from "react"; + +import { createCountryHandler } from "./createCountryHandler"; + +describe("createCountryHandler", () => { + it("calls original country handler and restets the country area field", () => { + // Arrange + const originalCountryHandler = jest.fn(); + const setFn = jest.fn(); + const exampleEvent = { target: "some event" } as ChangeEvent; + const newHandler = createCountryHandler(originalCountryHandler, setFn); + + // Act + newHandler(exampleEvent); + + // Assert + expect(originalCountryHandler).toBeCalledWith(exampleEvent); + expect(setFn).toBeCalledWith({ countryArea: "" }); + }); +}); diff --git a/src/components/AddressEdit/createCountryHandler.ts b/src/components/AddressEdit/createCountryHandler.ts new file mode 100644 index 000000000..06426d236 --- /dev/null +++ b/src/components/AddressEdit/createCountryHandler.ts @@ -0,0 +1,11 @@ +import { AddressTypeInput } from "@saleor/customers/types"; +import { FormChange } from "@saleor/hooks/useForm"; +import { ChangeEvent } from "react"; + +export const createCountryHandler = ( + currentHandler: FormChange, + set: (dataSet: Partial) => void, +) => (event: ChangeEvent) => { + currentHandler(event); + set({ countryArea: "" }); +}; diff --git a/src/components/AddressEdit/queries.ts b/src/components/AddressEdit/queries.ts new file mode 100644 index 000000000..8bc2b2997 --- /dev/null +++ b/src/components/AddressEdit/queries.ts @@ -0,0 +1,13 @@ +import { gql } from "@apollo/client"; + +export const addressValidationQuery = gql` + query addressValidationRules($countryCode: CountryCode!) { + addressValidationRules(countryCode: $countryCode) { + countryAreaChoices { + raw + verbose + } + allowedFields + } + } +`; diff --git a/src/components/AddressEdit/useAddressValidation.test.ts b/src/components/AddressEdit/useAddressValidation.test.ts new file mode 100644 index 000000000..728d38afc --- /dev/null +++ b/src/components/AddressEdit/useAddressValidation.test.ts @@ -0,0 +1,65 @@ +import { useAddressValidationRulesQuery } from "@saleor/graphql"; +import { renderHook } from "@testing-library/react-hooks"; + +import { useAddressValidation } from "./useAddressValidation"; + +jest.mock("@saleor/graphql", () => ({ + CountryCode: jest.requireActual("@saleor/graphql").CountryCode, + useAddressValidationRulesQuery: jest.fn(), +})); + +describe("useAddressValidation", () => { + it("skips loading validation rules when country is not provided", () => { + // Arrange + (useAddressValidationRulesQuery as jest.Mock).mockReturnValue({ + data: null, + loading: false, + }); + // Act + const { + result: { current }, + } = renderHook(() => useAddressValidation()); + + // Assert + expect(current.areas).toEqual([]); + expect(current.loading).toBeFalsy(); + expect(useAddressValidationRulesQuery).toBeCalledWith({ + skip: true, + variables: { countryCode: undefined }, + }); + expect(current.isFieldAllowed("country")).toBeFalsy(); + }); + + it("loads validation rules when country is provided", () => { + // Arrange + (useAddressValidationRulesQuery as jest.Mock).mockReturnValue({ + data: { + addressValidationRules: { + countryAreaChoices: [ + { raw: "AL", verbose: "Alabama" }, + { raw: "AN", verbose: "Ancona" }, + ], + allowedFields: ["country"], + }, + }, + loading: false, + }); + // Act + const { + result: { current }, + } = renderHook(() => useAddressValidation("US")); + + // Assert + expect(current.areas).toEqual([ + { label: "Alabama", value: "Alabama" }, + { label: "Ancona", value: "Ancona" }, + ]); + expect(current.loading).toBeFalsy(); + expect(useAddressValidationRulesQuery).toBeCalledWith({ + skip: false, + variables: { countryCode: "US" }, + }); + expect(current.isFieldAllowed("country")).toBeTruthy(); + expect(current.isFieldAllowed("countryArea")).toBeFalsy(); + }); +}); diff --git a/src/components/AddressEdit/useAddressValidation.ts b/src/components/AddressEdit/useAddressValidation.ts new file mode 100644 index 000000000..b352996ce --- /dev/null +++ b/src/components/AddressEdit/useAddressValidation.ts @@ -0,0 +1,58 @@ +import { + AddressValidationRulesQuery, + CountryCode, + useAddressValidationRulesQuery, +} from "@saleor/graphql"; +import { ChoiceValue } from "@saleor/sdk/dist/apollo/types"; + +const prepareChoices = (values: ChoiceValue[]) => + values.map(v => ({ + label: v.verbose, + value: v.verbose, + })); + +const selectRules = (data: AddressValidationRulesQuery) => + data + ? data.addressValidationRules + : { countryAreaChoices: [], allowedFields: [] }; + +const useValidationRules = (country?: string) => { + const countryCode = CountryCode[country]; + const { data, loading } = useAddressValidationRulesQuery({ + variables: { countryCode }, + skip: !countryCode, + }); + + return { data, loading }; +}; + +const useAreas = (data: AddressValidationRulesQuery) => { + const rawChoices = selectRules(data).countryAreaChoices; + const choices = prepareChoices(rawChoices); + + return choices; +}; + +const useAllowedFields = (data: AddressValidationRulesQuery) => { + const isAllowed = (fieldName: string) => { + if (!data) { + return false; + } + + return selectRules(data).allowedFields.includes(fieldName); + }; + + return { isAllowed }; +}; + +export const useAddressValidation = (country?: string) => { + const { data, loading } = useValidationRules(country); + const areas = useAreas(data); + const { isAllowed } = useAllowedFields(data); + + return { + areas, + isFieldAllowed: isAllowed, + loading, + }; +}; diff --git a/src/components/CompanyAddressInput/CompanyAddressForm.tsx b/src/components/CompanyAddressInput/CompanyAddressForm.tsx index d0e404c6c..ba307b0ad 100644 --- a/src/components/CompanyAddressInput/CompanyAddressForm.tsx +++ b/src/components/CompanyAddressInput/CompanyAddressForm.tsx @@ -19,6 +19,8 @@ import getWarehouseErrorMessage from "@saleor/utils/errors/warehouse"; import React from "react"; import { IntlShape, useIntl } from "react-intl"; +import { useAddressValidation } from "../AddressEdit/useAddressValidation"; + export interface CompanyAddressFormProps { countries: SingleAutocompleteChoiceType[]; data: AddressTypeInput; @@ -62,7 +64,7 @@ const CompanyAddressForm: React.FC = props => { onChange, onCountryChange, } = props; - + const { areas, isFieldAllowed } = useAddressValidation(data.country); const classes = useStyles(props); const intl = useIntl(); @@ -176,6 +178,7 @@ const CompanyAddressForm: React.FC = props => { = props => { choices={countries} InputProps={{ spellCheck: false, + autoComplete: "new-password", }} /> - + {isFieldAllowed("countryArea") && ( + + )} void; onChange: (event: React.ChangeEvent) => void; fetchOnFocus?: boolean; @@ -65,6 +66,7 @@ const SingleAutocompleteSelectFieldComponent: React.FC = ({ maxWidth="sm" >
- {({ change, data }) => { - const handleCountrySelect = createSingleAutocompleteSelectHandler( + {({ change, set, data }) => { + const countrySelect = createSingleAutocompleteSelectHandler( change, setCountryDisplayName, countryChoices, ); + const handleCountrySelect = createCountryHandler(countrySelect, set); + return ( <> diff --git a/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx b/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx index f4a4f9c3c..bebb4ec86 100644 --- a/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx +++ b/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx @@ -1,3 +1,4 @@ +import { createCountryHandler } from "@saleor/components/AddressEdit/createCountryHandler"; import { Backlink } from "@saleor/components/Backlink"; import { CardSpacer } from "@saleor/components/CardSpacer"; import Container from "@saleor/components/Container"; @@ -143,13 +144,15 @@ const CustomerCreatePage: React.FC = ({ onSubmit={handleSubmit} disabled={disabled} > - {({ change, data, isSaveDisabled, submit }) => { - const handleCountrySelect = createSingleAutocompleteSelectHandler( + {({ change, set, data, isSaveDisabled, submit }) => { + const countrySelect = createSingleAutocompleteSelectHandler( change, setCountryDisplayName, countryChoices, ); + const handleCountrySelect = createCountryHandler(countrySelect, set); + return ( diff --git a/src/graphql/hooks.generated.ts b/src/graphql/hooks.generated.ts index 152be51b7..3f9e671be 100644 --- a/src/graphql/hooks.generated.ts +++ b/src/graphql/hooks.generated.ts @@ -5039,6 +5039,45 @@ export function useCollectionDetailsLazyQuery(baseOptions?: ApolloReactHooks.Laz export type CollectionDetailsQueryHookResult = ReturnType; export type CollectionDetailsLazyQueryHookResult = ReturnType; export type CollectionDetailsQueryResult = Apollo.QueryResult; +export const AddressValidationRulesDocument = gql` + query addressValidationRules($countryCode: CountryCode!) { + addressValidationRules(countryCode: $countryCode) { + countryAreaChoices { + raw + verbose + } + allowedFields + } +} + `; + +/** + * __useAddressValidationRulesQuery__ + * + * To run a query within a React component, call `useAddressValidationRulesQuery` and pass it any options that fit your needs. + * When your component renders, `useAddressValidationRulesQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useAddressValidationRulesQuery({ + * variables: { + * countryCode: // value for 'countryCode' + * }, + * }); + */ +export function useAddressValidationRulesQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(AddressValidationRulesDocument, options); + } +export function useAddressValidationRulesLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(AddressValidationRulesDocument, options); + } +export type AddressValidationRulesQueryHookResult = ReturnType; +export type AddressValidationRulesLazyQueryHookResult = ReturnType; +export type AddressValidationRulesQueryResult = Apollo.QueryResult; export const CheckIfOrderExistsDocument = gql` query CheckIfOrderExists($id: ID!) { order(id: $id) { diff --git a/src/graphql/types.generated.ts b/src/graphql/types.generated.ts index de219f0f7..543e71f5d 100644 --- a/src/graphql/types.generated.ts +++ b/src/graphql/types.generated.ts @@ -6670,6 +6670,13 @@ export type CollectionDetailsQueryVariables = Exact<{ export type CollectionDetailsQuery = { __typename: 'Query', collection: { __typename: 'Collection', slug: string, description: any | null, seoDescription: string | null, seoTitle: string | null, id: string, name: string, products: { __typename: 'ProductCountableConnection', edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', id: string, name: string, productType: { __typename: 'ProductType', id: string, name: string }, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, backgroundImage: { __typename: 'Image', alt: string | null, url: string } | null, channelListings: Array<{ __typename: 'CollectionChannelListing', isPublished: boolean, publicationDate: any | null, channel: { __typename: 'Channel', id: string, name: string } }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null }; +export type AddressValidationRulesQueryVariables = Exact<{ + countryCode: CountryCode; +}>; + + +export type AddressValidationRulesQuery = { __typename: 'Query', addressValidationRules: { __typename: 'AddressValidationData', allowedFields: Array, countryAreaChoices: Array<{ __typename: 'ChoiceValue', raw: string | null, verbose: string | null }> } | null }; + export type CheckIfOrderExistsQueryVariables = Exact<{ id: Scalars['ID']; }>; diff --git a/src/orders/components/OrderCustomerAddressesEditDialog/form.tsx b/src/orders/components/OrderCustomerAddressesEditDialog/form.tsx index f3d8d5710..0df8edbd2 100644 --- a/src/orders/components/OrderCustomerAddressesEditDialog/form.tsx +++ b/src/orders/components/OrderCustomerAddressesEditDialog/form.tsx @@ -145,6 +145,7 @@ function useOrderCustomerAddressesEditForm( value: { ...formData.shippingAddress, [event.target.name]: event.target.value, + countryArea: "", }, }, }), @@ -159,6 +160,7 @@ function useOrderCustomerAddressesEditForm( value: { ...formData.billingAddress, [event.target.name]: event.target.value, + countryArea: "", }, }, }), diff --git a/src/siteSettings/components/SiteSettingsPage/SiteSettingsPage.tsx b/src/siteSettings/components/SiteSettingsPage/SiteSettingsPage.tsx index fff9fc324..f949dfa37 100644 --- a/src/siteSettings/components/SiteSettingsPage/SiteSettingsPage.tsx +++ b/src/siteSettings/components/SiteSettingsPage/SiteSettingsPage.tsx @@ -1,3 +1,4 @@ +import { createCountryHandler } from "@saleor/components/AddressEdit/createCountryHandler"; import { Backlink } from "@saleor/components/Backlink"; import CompanyAddressInput from "@saleor/components/CompanyAddressInput"; import Container from "@saleor/components/Container"; @@ -126,14 +127,16 @@ const SiteSettingsPage: React.FC = props => { confirmLeave disabled={disabled} > - {({ change, data, isSaveDisabled, submit }) => { + {({ change, data, set, isSaveDisabled, submit }) => { const countryChoices = mapCountriesToChoices(shop?.countries || []); - const handleCountryChange = createSingleAutocompleteSelectHandler( + const countrySelect = createSingleAutocompleteSelectHandler( change, setDisplayCountry, countryChoices, ); + const handleCountrySelect = createCountryHandler(countrySelect, set); + return ( @@ -175,7 +178,7 @@ const SiteSettingsPage: React.FC = props => { description: "section header", })} onChange={change} - onCountryChange={handleCountryChange} + onCountryChange={handleCountrySelect} /> = ({ return ( - {({ change, data, submit }) => { + {({ change, data, set, submit }) => { const countryChoices = mapCountriesToChoices(countries); - const handleCountryChange = createSingleAutocompleteSelectHandler( + const countrySelect = createSingleAutocompleteSelectHandler( change, setDisplayCountry, countryChoices, ); + const handleCountrySelect = createCountryHandler(countrySelect, set); + return ( @@ -108,7 +111,7 @@ const WarehouseCreatePage: React.FC = ({ description: "warehouse", })} onChange={change} - onCountryChange={handleCountryChange} + onCountryChange={handleCountrySelect} /> diff --git a/src/warehouses/components/WarehouseDetailsPage/WarehouseDetailsPage.tsx b/src/warehouses/components/WarehouseDetailsPage/WarehouseDetailsPage.tsx index b583dea14..536123ef5 100644 --- a/src/warehouses/components/WarehouseDetailsPage/WarehouseDetailsPage.tsx +++ b/src/warehouses/components/WarehouseDetailsPage/WarehouseDetailsPage.tsx @@ -1,3 +1,4 @@ +import { createCountryHandler } from "@saleor/components/AddressEdit/createCountryHandler"; import { Backlink } from "@saleor/components/Backlink"; import CardSpacer from "@saleor/components/CardSpacer"; import CompanyAddressInput from "@saleor/components/CompanyAddressInput"; @@ -93,11 +94,12 @@ const WarehouseDetailsPage: React.FC = ({ > {({ change, data, isSaveDisabled, submit, set }) => { const countryChoices = mapCountriesToChoices(countries); - const handleCountryChange = createSingleAutocompleteSelectHandler( + const countrySelect = createSingleAutocompleteSelectHandler( change, setDisplayCountry, countryChoices, ); + const handleCountrySelect = createCountryHandler(countrySelect, set); return ( @@ -126,7 +128,7 @@ const WarehouseDetailsPage: React.FC = ({ description: "warehouse", })} onChange={change} - onCountryChange={handleCountryChange} + onCountryChange={handleCountrySelect} />