From 65cfb35a7a21cfca9c54970f16f1dcf95e56ec16 Mon Sep 17 00:00:00 2001 From: vapersmile <120370047+vapersmile@users.noreply.github.com> Date: Wed, 17 Apr 2024 17:24:10 +0200 Subject: [PATCH] feat(redmine 1291405): add AddressAutocompleteFields (#174) * feat(redmine 1291405): add AddressAutocompleteFields * feat(redmine 1291405): fix types errors --------- Co-authored-by: pereag --- .changeset/stale-dryers-move.md | 5 + .../AddressAutocompleteFields.mock.ts | 16 ++ .../AddressAutocompleteFields.module.css | 14 ++ .../AddressAutocompleteFields.stories.tsx | 26 +++ .../AddressAutocompleteFields.test.tsx | 34 ++++ .../AddressAutocompleteFields.tsx | 184 ++++++++++++++++++ .../AddressAutocompleteFields.test.tsx.snap | 167 ++++++++++++++++ .../AddressGouvAutocompleteField.tsx | 14 +- .../AddressGouvApi.stories.tsx | 23 +++ .../FetchAutoCompleteField.mock.tsx | 19 +- .../FetchAutocompleteField.tsx | 16 +- ....stories.tsx => OpenStreetMap.stories.tsx} | 19 +- 12 files changed, 504 insertions(+), 33 deletions(-) create mode 100644 .changeset/stale-dryers-move.md create mode 100644 packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.mock.ts create mode 100644 packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.module.css create mode 100644 packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.stories.tsx create mode 100644 packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.test.tsx create mode 100644 packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.tsx create mode 100644 packages/react-front-kit/src/Form/AddressAutocompleteFields/__snapshots__/AddressAutocompleteFields.test.tsx.snap create mode 100644 packages/react-front-kit/src/Form/FetchAutocompleteField/AddressGouvApi.stories.tsx rename packages/react-front-kit/src/Form/FetchAutocompleteField/{FetchAutocompleteField.stories.tsx => OpenStreetMap.stories.tsx} (57%) diff --git a/.changeset/stale-dryers-move.md b/.changeset/stale-dryers-move.md new file mode 100644 index 00000000..3d694355 --- /dev/null +++ b/.changeset/stale-dryers-move.md @@ -0,0 +1,5 @@ +--- +'@smile/react-front-kit': minor +--- + +Add AddressAutocompleteFields diff --git a/packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.mock.ts b/packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.mock.ts new file mode 100644 index 00000000..7d5d6c2a --- /dev/null +++ b/packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.mock.ts @@ -0,0 +1,16 @@ +import type { IAdressFields } from './AddressAutocompleteFields'; +import type { IAddressGouvData } from '../FetchAutocompleteField/FetchAutoCompleteField.mock'; +import type { IValue } from '../FetchAutocompleteField/FetchAutocompleteField'; + +export function onOptionSubmitMock( + value: IValue, +): IAdressFields { + const address = value.value.properties; + return { + city: address.city, + country: 'France', + number: address.housenumber, + postCode: address.postcode, + street: address.street, + }; +} diff --git a/packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.module.css b/packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.module.css new file mode 100644 index 00000000..98870e01 --- /dev/null +++ b/packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.module.css @@ -0,0 +1,14 @@ +.inputContainer { + display: flex; + gap: 10px; + margin-top: 20px; + flex-wrap: wrap; +} + +.input { + maxwidth: 300px; + width: calc(50% - 5px); + @mixin smaller-than $mantine-breakpoint-sm { + width: 100%; + } +} diff --git a/packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.stories.tsx b/packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.stories.tsx new file mode 100644 index 00000000..80a404a8 --- /dev/null +++ b/packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.stories.tsx @@ -0,0 +1,26 @@ +import type { IAddressGouvData } from '../FetchAutocompleteField/FetchAutoCompleteField.mock'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { action } from '@storybook/addon-actions'; + +import { getDataAddressGouvMock } from '../FetchAutocompleteField/FetchAutoCompleteField.mock'; + +import { AddressAutocompleteFields as Cmp } from './AddressAutocompleteFields'; +import { onOptionSubmitMock } from './AddressAutocompleteFields.mock'; + +const meta = { + component: Cmp, + tags: ['autodocs'], + title: '3-custom/Form/AddressAutocompleteFields', +} satisfies Meta>; + +export default meta; +type IStory = StoryObj; + +export const AddressAutocompleteFields: IStory = { + args: { + onFetchData: getDataAddressGouvMock, + onFieldsValuesChange: action('value change'), + onOptionSubmit: onOptionSubmitMock, + }, +}; diff --git a/packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.test.tsx b/packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.test.tsx new file mode 100644 index 00000000..4d9d8fbb --- /dev/null +++ b/packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.test.tsx @@ -0,0 +1,34 @@ +import type { IAdressFields } from './AddressAutocompleteFields'; +import type { IValue } from '../FetchAutocompleteField/FetchAutocompleteField'; + +import { renderWithProviders } from '@smile/react-front-kit-shared/test-utils'; + +import { AddressAutocompleteFields } from './AddressAutocompleteFields'; + +describe('FetchAutocompleteField', () => { + beforeEach(() => { + // Prevent mantine random ID + Math.random = () => 0.42; + }); + it('matches snapshot', () => { + const { container } = renderWithProviders( + []> { + return [ + { label: 'test', value: { Address: 'test', Number: 'test' } }, + ] as unknown as Promise[]>; + }} + onOptionSubmit={function (_value: unknown): IAdressFields { + return { + city: 'city', + country: 'country', + number: 'number', + postCode: 'postCode', + street: 'street', + }; + }} + />, + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.tsx b/packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.tsx new file mode 100644 index 00000000..4c749883 --- /dev/null +++ b/packages/react-front-kit/src/Form/AddressAutocompleteFields/AddressAutocompleteFields.tsx @@ -0,0 +1,184 @@ +'use client'; +import type { + IFetchAutocompleteFieldProps, + IValue, +} from '../FetchAutocompleteField/FetchAutocompleteField'; +import type { TextInputProps } from '@mantine/core'; +import type { ReactElement } from 'react'; + +import { TextInput } from '@mantine/core'; +import { useState } from 'react'; + +import { FetchAutocompleteField } from '../FetchAutocompleteField/FetchAutocompleteField'; + +import classes from './AddressAutocompleteFields.module.css'; + +export interface IAdressFields { + city?: string; + country?: string; + number?: string; + postCode?: string; + street?: string; +} + +export interface ICity { + description?: string; + label?: string; + placeholder?: string; +} +export interface ICountry { + description?: string; + label?: string; + placeholder?: string; +} +export interface INumber { + description?: string; + label?: string; + placeholder?: string; +} +export interface IPostcode { + description?: string; + label?: string; + placeholder?: string; +} + +export interface IStreet { + description?: string; + label?: string; + placeholder?: string; +} + +export interface IAddressAutocompleteFieldsProps + extends Omit, 'onOptionSubmit'> { + city?: ICity; + country?: ICountry; + number?: INumber; + onFieldsValuesChange?: (value: IAdressFields) => void; + onOptionSubmit: (value: IValue) => IAdressFields; + postCode?: IPostcode; + street?: IStreet; + textInputProps?: TextInputProps; +} + +export function AddressAutocompleteFields( + props: IAddressAutocompleteFieldsProps, +): ReactElement { + const { + street = { + label: 'Street Name', + placeholder: 'Pall Mall', + }, + city = { + label: 'City', + placeholder: 'London', + }, + country = { label: 'Country', placeholder: 'United Kingdom' }, + number = { label: 'Street number', placeholder: '89' }, + onOptionSubmit, + onFieldsValuesChange, + postCode = { + label: 'Postal code', + placeholder: 'SW1Y 5HS', + }, + textInputProps, + ...fetchAutocompleteFieldProps + } = props; + + const [streetValue, setStreetValue] = useState(''); + const [numberValue, setNumberValue] = useState(''); + const [cityValue, setCityValue] = useState(''); + const [postCodeValue, setPostCodeValue] = useState(''); + const [countryValue, setCountryValue] = useState(''); + + function onOptionSubmitHandle(value: IValue): void { + const addressFields = onOptionSubmit(value); + setStreetValue(addressFields.street ?? ''); + setNumberValue(addressFields.number ?? ''); + setCityValue(addressFields.city ?? ''); + setPostCodeValue(addressFields.postCode ?? ''); + setCountryValue(addressFields.country ?? ''); + } + + function onChangeHandle(label: string, value: string): void { + onFieldsValuesChange?.({ [label]: value }); + } + + const inputs = [ + { + description: street.description, + label: street.label, + onchange: (e: React.ChangeEvent) => { + setStreetValue(e.target.value); + onChangeHandle('street', e.target.value); + }, + placeholder: street.placeholder, + value: streetValue, + }, + { + description: number.description, + label: number.label, + onchange: (e: React.ChangeEvent) => { + setNumberValue(e.target.value); + onChangeHandle('number', e.target.value); + }, + placeholder: number.placeholder, + value: numberValue, + }, + { + description: city.description, + label: city.label, + onchange: (e: React.ChangeEvent) => { + setCityValue(e.target.value); + onChangeHandle('city', e.target.value); + }, + placeholder: city.placeholder, + value: cityValue, + }, + { + description: postCode.description, + label: postCode.label, + onchange: (e: React.ChangeEvent) => { + setPostCodeValue(e.target.value); + onChangeHandle('postCode', e.target.value); + }, + placeholder: postCode.placeholder, + value: postCodeValue, + }, + { + description: country.description, + label: country.label, + onchange: (e: React.ChangeEvent) => { + setCountryValue(e.target.value); + onChangeHandle('country', e.target.value); + }, + placeholder: country.placeholder, + value: countryValue, + }, + ]; + + return ( +
+ onOptionSubmitHandle(value)} + {...fetchAutocompleteFieldProps} + /> +
+ {inputs.map((input) => { + return ( + { + input.onchange(e); + }} + placeholder={input.placeholder} + value={input.value} + /> + ); + })} +
+
+ ); +} diff --git a/packages/react-front-kit/src/Form/AddressAutocompleteFields/__snapshots__/AddressAutocompleteFields.test.tsx.snap b/packages/react-front-kit/src/Form/AddressAutocompleteFields/__snapshots__/AddressAutocompleteFields.test.tsx.snap new file mode 100644 index 00000000..c1beeac4 --- /dev/null +++ b/packages/react-front-kit/src/Form/AddressAutocompleteFields/__snapshots__/AddressAutocompleteFields.test.tsx.snap @@ -0,0 +1,167 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FetchAutocompleteField matches snapshot 1`] = ` +
+ + +
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+`; diff --git a/packages/react-front-kit/src/Form/AddressGouvAutocompleteField/AddressGouvAutocompleteField.tsx b/packages/react-front-kit/src/Form/AddressGouvAutocompleteField/AddressGouvAutocompleteField.tsx index 77e44720..767a309b 100644 --- a/packages/react-front-kit/src/Form/AddressGouvAutocompleteField/AddressGouvAutocompleteField.tsx +++ b/packages/react-front-kit/src/Form/AddressGouvAutocompleteField/AddressGouvAutocompleteField.tsx @@ -9,17 +9,17 @@ import type { ReactElement } from 'react'; import { FetchAutocompleteField } from '../FetchAutocompleteField/FetchAutocompleteField'; -export interface IAddressAutocompleteFieldProps - extends Omit, 'onFetchData'> { +export interface IAddressAutocompleteFieldProps + extends Omit, 'onFetchData'> { lat?: string; limit?: number; lon?: string; - onFetchData?: (value: string) => Promise[]>; + onFetchData?: (value: string) => Promise[]>; type?: string; } -export function AddressGouvAutocompleteField( - props: IAddressAutocompleteFieldProps, +export function AddressGouvAutocompleteField( + props: IAddressAutocompleteFieldProps, ): ReactElement { const { lat = '', @@ -28,7 +28,9 @@ export function AddressGouvAutocompleteField( type = '', ...fetchAutocompleteFieldProps } = props; - async function getDataAddressGouv(value: string): Promise[]> { + async function getDataAddressGouv( + value: string, + ): Promise[]> { const response = await fetch( `https://api-Adresse.data.gouv.fr/search/?q=${encodeURIComponent( value, diff --git a/packages/react-front-kit/src/Form/FetchAutocompleteField/AddressGouvApi.stories.tsx b/packages/react-front-kit/src/Form/FetchAutocompleteField/AddressGouvApi.stories.tsx new file mode 100644 index 00000000..617d0ebc --- /dev/null +++ b/packages/react-front-kit/src/Form/FetchAutocompleteField/AddressGouvApi.stories.tsx @@ -0,0 +1,23 @@ +import type { IAddressGouvData } from './FetchAutoCompleteField.mock'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { action } from '@storybook/addon-actions'; + +import { getDataAddressGouvMock } from './FetchAutoCompleteField.mock'; +import { FetchAutocompleteField as Cmp } from './FetchAutocompleteField'; + +const meta = { + component: Cmp, + tags: ['autodocs'], + title: '3-custom/Form/FetchAutocompleteField', +} satisfies Meta>; + +export default meta; +type IStory = StoryObj; + +export const FieldWithAddressGouvApi: IStory = { + args: { + onFetchData: getDataAddressGouvMock, + onOptionSubmit: action('location'), + }, +}; diff --git a/packages/react-front-kit/src/Form/FetchAutocompleteField/FetchAutoCompleteField.mock.tsx b/packages/react-front-kit/src/Form/FetchAutocompleteField/FetchAutoCompleteField.mock.tsx index d84117fe..079347df 100644 --- a/packages/react-front-kit/src/Form/FetchAutocompleteField/FetchAutoCompleteField.mock.tsx +++ b/packages/react-front-kit/src/Form/FetchAutocompleteField/FetchAutoCompleteField.mock.tsx @@ -1,29 +1,40 @@ +import type { IValue } from './FetchAutocompleteField'; + /* eslint-disable @typescript-eslint/naming-convention */ export interface IOpenStreetMapData { display_name: string; } export interface IAddressGouvData { - properties: { label: string }; + properties: { + city?: string; + housenumber?: string; + label: string; + number?: string; + postcode?: string; + street?: string; + }; } export async function getDataOpenStreetMapMock( value: string, -): Promise { +): Promise[]> { const response = await fetch( `https://nominatim.openstreetmap.org/search.php?q=${encodeURIComponent( value, )}&format=jsonv2&addressdetails=1&countrycodes=fr&accept-language=fr&limit=10&dedupe=1`, ); const data: IOpenStreetMapData[] = await response.json(); - const result = data.map((element) => { + const result: IValue[] = data.map((element) => { return { label: element.display_name, value: element }; }); return result; } -export async function getDataAddressGouvMock(value: string): Promise { +export async function getDataAddressGouvMock( + value: string, +): Promise[]> { const response = await fetch( `https://api-Adresse.data.gouv.fr/search/?q=${encodeURIComponent( value, diff --git a/packages/react-front-kit/src/Form/FetchAutocompleteField/FetchAutocompleteField.tsx b/packages/react-front-kit/src/Form/FetchAutocompleteField/FetchAutocompleteField.tsx index 8c00faca..fc874dfc 100644 --- a/packages/react-front-kit/src/Form/FetchAutocompleteField/FetchAutocompleteField.tsx +++ b/packages/react-front-kit/src/Form/FetchAutocompleteField/FetchAutocompleteField.tsx @@ -13,21 +13,21 @@ export interface IFetchOption { value: string; } -export interface IValue { +export interface IValue { label: string; - value: F; + value: T; } -export interface IFetchAutocompleteFieldProps +export interface IFetchAutocompleteFieldProps extends Omit { deDebounce?: number; minValueLength?: number; - onFetchData: (value: string) => Promise[]>; - onOptionSubmit?: (value: unknown) => void; + onFetchData: (value: string) => Promise[]>; + onOptionSubmit?: (value: IValue) => void; } -export function FetchAutocompleteField( - props: IFetchAutocompleteFieldProps, +export function FetchAutocompleteField( + props: IFetchAutocompleteFieldProps, ): ReactElement { const { deDebounce = 1000, @@ -38,7 +38,7 @@ export function FetchAutocompleteField( onFetchData, ...autocompleteProps } = props; - const [data, setData] = useState[]>([]); + const [data, setData] = useState[]>([]); const [value, setValue] = useDebouncedState('', deDebounce); useEffect(() => { diff --git a/packages/react-front-kit/src/Form/FetchAutocompleteField/FetchAutocompleteField.stories.tsx b/packages/react-front-kit/src/Form/FetchAutocompleteField/OpenStreetMap.stories.tsx similarity index 57% rename from packages/react-front-kit/src/Form/FetchAutocompleteField/FetchAutocompleteField.stories.tsx rename to packages/react-front-kit/src/Form/FetchAutocompleteField/OpenStreetMap.stories.tsx index 8330301e..47d15627 100644 --- a/packages/react-front-kit/src/Form/FetchAutocompleteField/FetchAutocompleteField.stories.tsx +++ b/packages/react-front-kit/src/Form/FetchAutocompleteField/OpenStreetMap.stories.tsx @@ -1,34 +1,23 @@ +import type { IOpenStreetMapData } from './FetchAutoCompleteField.mock'; import type { Meta, StoryObj } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { - getDataAddressGouvMock, - getDataOpenStreetMapMock, -} from './FetchAutoCompleteField.mock'; +import { getDataOpenStreetMapMock } from './FetchAutoCompleteField.mock'; import { FetchAutocompleteField as Cmp } from './FetchAutocompleteField'; const meta = { - component: Cmp, + component: Cmp, tags: ['autodocs'], title: '3-custom/Form/FetchAutocompleteField', -} satisfies Meta; +} satisfies Meta>; export default meta; type IStory = StoryObj; export const FieldWithOpenStreetMapApi: IStory = { args: { - // @ts-expect-error-type onFetchData: getDataOpenStreetMapMock, onOptionSubmit: action('location'), }, }; - -export const FieldWithAddressGouvApi: IStory = { - args: { - // @ts-expect-error-type - onFetchData: getDataAddressGouvMock, - onOptionSubmit: action('location'), - }, -};