Skip to content

Commit

Permalink
Merge pull request #637 from commercelayer/use-resource-address-overlay
Browse files Browse the repository at this point in the history
Add `useResourceAddressOverlay` hook and `ResourceAddressFormFields` component can be reused in other forms
  • Loading branch information
marcomontalbano committed Apr 30, 2024
2 parents 86407f0 + cac86f3 commit 51e5681
Show file tree
Hide file tree
Showing 14 changed files with 494 additions and 262 deletions.
2 changes: 1 addition & 1 deletion packages/app-elements/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"swr": "^2.2.5",
"ts-invariant": "^0.10.3",
"type-fest": "^4.15.0",
"zod": "^3.22.4"
"zod": "^3.23.4"
},
"devDependencies": {
"@commercelayer/eslint-config-ts-react": "^1.4.5",
Expand Down
4 changes: 4 additions & 0 deletions packages/app-elements/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,10 @@ export {
// Resources
export {
ResourceAddress,
ResourceAddressFormFields,
resourceAddressFormFieldsSchema,
useResourceAddressOverlay,
type ResourceAddressFormFieldsProps,
type ResourceAddressProps
} from '#ui/resources/ResourceAddress'
export {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type Address } from '@commercelayer/sdk'
export const presetAddresses = {
withName: {
type: 'addresses',
id: '',
id: 'aaZYuDJVXW',
company: '',
first_name: 'Darth',
last_name: 'Vader',
Expand All @@ -21,7 +21,7 @@ export const presetAddresses = {
},
withCompany: {
type: 'addresses',
id: '',
id: 'bbZYuDJVXW',
company: 'Galactic Empire',
first_name: '',
last_name: '',
Expand All @@ -39,12 +39,12 @@ export const presetAddresses = {
},
withNotes: {
type: 'addresses',
id: '',
id: 'ccZYuDJVXW',
company: '',
first_name: 'Darth',
last_name: 'Vader',
full_name: 'Darth Vader',
line_1: 'Via Morte Nera, 13',
first_name: 'Luke',
last_name: 'Skywalker',
full_name: 'Luke Skywalker',
line_1: 'Via Polis Massa, 42',
line_2: 'Ragnatela, 99',
city: 'Cogorno',
country_code: 'IT',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@ describe('ResourceAddress', () => {

test('Should render fullName', async () => {
const { getByTestId } = setup()
expect(getByTestId('ResourceAddress-fullName')).toContainHTML('Darth Vader')
expect(getByTestId('ResourceAddress-fullName')).toContainHTML(
'Luke Skywalker'
)
})

test('Should render address', async () => {
const { getByTestId } = setup()
expect(getByTestId('ResourceAddress-address')).toContainHTML(
'Via Morte Nera, 13'
'Via Polis Massa, 42'
)
})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { useOverlay } from '#hooks/useOverlay'
import { useTokenProvider } from '#providers/TokenProvider'
import { Button } from '#ui/atoms/Button'
import { Hr } from '#ui/atoms/Hr'
import { withSkeletonTemplate } from '#ui/atoms/SkeletonTemplate'
import { Spacer } from '#ui/atoms/Spacer'
import { Text } from '#ui/atoms/Text'
import { PageLayout } from '#ui/composite/PageLayout'
import { type Address } from '@commercelayer/sdk'
import { Note, PencilSimple, Phone } from '@phosphor-icons/react'
import isEmpty from 'lodash/isEmpty'
import { useEffect, useState } from 'react'
import { ResourceAddressForm } from './ResourceAddressForm'
import { useResourceAddressOverlay } from './useResourceAddressOverlay'

export interface ResourceAddressProps {
/**
Expand Down Expand Up @@ -38,10 +36,17 @@ export interface ResourceAddressProps {
*/
export const ResourceAddress = withSkeletonTemplate<ResourceAddressProps>(
({ resource, title, editable = false, showBillingInfo = false }) => {
const { Overlay, open, close } = useOverlay()
const [address, setAddress] = useState<Address>(resource)
const { canUser } = useTokenProvider()

const [address, setAddress] = useState<Address>(resource)
const { ResourceAddressOverlay, openAddressOverlay } =
useResourceAddressOverlay({
address: resource,
showBillingInfo,
onUpdate: (updatedAddress) => {
setAddress(updatedAddress)
}
})

useEffect(() => {
setAddress(resource)
Expand Down Expand Up @@ -123,7 +128,7 @@ export const ResourceAddress = withSkeletonTemplate<ResourceAddressProps>(
<Button
variant='link'
onClick={() => {
open()
openAddressOverlay()
}}
data-testid='ResourceAddress-editButton'
>
Expand All @@ -132,29 +137,7 @@ export const ResourceAddress = withSkeletonTemplate<ResourceAddressProps>(
</div>
)}
</div>
{editable && canUser('update', 'addresses') && (
<Overlay>
<PageLayout
title='Edit address'
minHeight={false}
navigationButton={{
label: 'Back',
onClick: () => {
close()
}
}}
>
<ResourceAddressForm
address={address}
showBillingInfo={showBillingInfo}
onChange={(updatedAddress: Address) => {
setAddress(updatedAddress)
close()
}}
/>
</PageLayout>
</Overlay>
)}
{editable && <ResourceAddressOverlay />}
</>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,22 @@
import { useCoreSdkProvider } from '#providers/CoreSdkProvider'
import { Button } from '#ui/atoms/Button'
import { Grid } from '#ui/atoms/Grid'
import { withSkeletonTemplate } from '#ui/atoms/SkeletonTemplate'
import { Spacer } from '#ui/atoms/Spacer'
import { HookedForm } from '#ui/forms/Form/HookedForm'
import { HookedInput } from '#ui/forms/Input/HookedInput'
import { type InputSelectValue } from '#ui/forms/InputSelect'
import { HookedInputSelect } from '#ui/forms/InputSelect/HookedInputSelect'
import { HookedInputTextArea } from '#ui/forms/InputTextArea'
import { HookedValidationApiError } from '#ui/forms/ReactHookForm/HookedValidationApiError'
import { type Address } from '@commercelayer/sdk'
import { zodResolver } from '@hookform/resolvers/zod'
import React, { useEffect, useState } from 'react'
import { useForm, useFormContext } from 'react-hook-form'
import { z } from 'zod'

const zodString = z
.string({
required_error: 'Required field',
invalid_type_error: 'Invalid format'
})
.min(1, {
message: 'Required field'
})

const addressFormSchema = z.object({
first_name: z.string().nullish(),
last_name: z.string().nullish(),
company: z.string().nullish(),
line_1: zodString,
line_2: z.string().nullish(),
city: zodString,
zip_code: z.string().nullish(),
state_code: zodString,
country_code: zodString,
phone: zodString,
billing_info: z.string().nullish(),
notes: z.string().nullish()
})

export type ResourceAddressFormValues = z.infer<typeof addressFormSchema>

interface ResourceAddressFormProps {
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import {
ResourceAddressFormFields,
resourceAddressFormFieldsSchema,
type ResourceAddressFormFieldsProps
} from './ResourceAddressFormFields'

interface ResourceAddressFormProps
extends Omit<ResourceAddressFormFieldsProps, 'name'> {
address: Address
showBillingInfo?: boolean
onChange: (updatedAddress: Address) => void
}

Expand All @@ -52,7 +25,7 @@ export const ResourceAddressForm =
({ address, showBillingInfo = false, onChange }) => {
const methods = useForm({
defaultValues: address,
resolver: zodResolver(addressFormSchema)
resolver: zodResolver(resourceAddressFormFieldsSchema)
})

const [apiError, setApiError] = useState<any>()
Expand All @@ -77,51 +50,7 @@ export const ResourceAddressForm =
})
}}
>
<FieldRow columns='2'>
<HookedInput name='first_name' label='First name' />
<HookedInput name='last_name' label='Last name' />
</FieldRow>

<FieldRow columns='1'>
<HookedInput name='company' label='Company' />
</FieldRow>

<FieldRow columns='1'>
<HookedInput name='line_1' label='Address line 1' />
</FieldRow>

<FieldRow columns='1'>
<HookedInput name='line_2' label='Address line 2' />
</FieldRow>

<FieldRow columns='1'>
<SelectCountry />
</FieldRow>

<FieldRow columns='1'>
<HookedInput name='city' label='City' />
</FieldRow>

<FieldRow columns='1'>
<div className='grid grid-cols-[2fr_1fr] gap-4'>
<SelectStates />
<HookedInput name='zip_code' label='ZIP code' />
</div>
</FieldRow>

<FieldRow columns='1'>
<HookedInput name='phone' label='Phone' />
</FieldRow>

{showBillingInfo && (
<FieldRow columns='1'>
<HookedInput name='billing_info' label='Billing info' />
</FieldRow>
)}

<FieldRow columns='1'>
<HookedInputTextArea name='notes' label='Notes' rows={2} />
</FieldRow>
<ResourceAddressFormFields showBillingInfo={showBillingInfo} />

<Spacer top='14'>
<Button type='submit' disabled={isSubmitting} className='w-full'>
Expand All @@ -133,98 +62,3 @@ export const ResourceAddressForm =
)
}
)

const FieldRow = ({
children,
columns
}: {
children: React.ReactNode
columns: '1' | '2'
}): JSX.Element => {
return (
<Spacer bottom='8'>
<Grid columns={columns}>{children}</Grid>
</Spacer>
)
}

const SelectCountry: React.FC = () => {
const [countries, setCountries] = useState<InputSelectValue[] | undefined>()
const [forceTextInput, setForceTextInput] = useState(false)

useEffect(() => {
void fetch('https://data.commercelayer.app/assets/lists/countries.json')
.then<InputSelectValue[]>(async (res) => await res.json())
.then((data) => {
setCountries(data)
})
.catch(() => {
// error fetching states, fallback to text input
setForceTextInput(true)
})
}, [])

if (forceTextInput) {
return <HookedInput name='country_code' label='Country' />
}

return (
<HookedInputSelect
name='country_code'
label='Country'
key={countries?.length}
initialValues={countries ?? []}
pathToValue='value'
isLoading={countries == null}
/>
)
}

const SelectStates: React.FC = () => {
const [states, setStates] = useState<InputSelectValue[] | undefined>()
const { watch, setValue } = useFormContext<ResourceAddressFormValues>()
const [forceTextInput, setForceTextInput] = useState(false)

const countryCode = watch('country_code')
const stateCode = watch('state_code')
const countryWithStates = ['US', 'IT']

useEffect(() => {
if (countryCode != null && countryWithStates.includes(countryCode)) {
void fetch(
`https://data.commercelayer.app/assets/lists/states/${countryCode}.json`
)
.then<InputSelectValue[]>(async (res) => await res.json())
.then((data) => {
setStates(data)
if (data.find(({ value }) => value === stateCode) == null) {
// reset state_code if not found in the list
setValue('state_code', '')
}
})
.catch(() => {
// error fetching states, fallback to text input
setForceTextInput(true)
})
}
}, [countryCode])

if (
!countryWithStates.includes(countryCode) ||
states?.length === 0 ||
forceTextInput
) {
return <HookedInput name='state_code' label='State code' />
}

return (
<HookedInputSelect
name='state_code'
label='State'
key={`${countryCode}_${states?.length}`}
initialValues={states ?? []}
pathToValue='value'
isLoading={states == null}
/>
)
}
Loading

0 comments on commit 51e5681

Please sign in to comment.