feat: add Google Place Autocomplete for address fields (#13450)
resolve #13253 This PR enables the use of Google Place Autocomplete and Place Details APIs in the backend. It allows users to automatically fill in address fields by typing into the address1 input. The input is debounced, then the Autocomplete API is called. Once the user selects an address, the Place Details API is used to parse and fill in the individual address fields. https://github.com/user-attachments/assets/e04b8474-25b8-48f5-83d0-2074f8d5fc94 --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { SELECT_AUTOCOMPLETE_LIST_DROPDOWN_ID } from '@/geo-map/constants/selectAutocompleteListDropDownId';
|
||||
import { useGetPlaceApiData } from '@/geo-map/hooks/useGetPlaceApiData';
|
||||
import { PlaceAutocompleteResult } from '@/geo-map/types/placeApi';
|
||||
import { FieldAddressDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { useCountryUtils } from './useCountryUtils';
|
||||
|
||||
export const useAddressAutocomplete = (
|
||||
onChange?: (updatedValue: FieldAddressDraftValue) => void,
|
||||
) => {
|
||||
const [placeAutocompleteData, setPlaceAutocompleteData] = useState<
|
||||
PlaceAutocompleteResult[] | null
|
||||
>([]);
|
||||
const [tokenForPlaceApi, setTokenForPlaceApi] = useState<string | null>(null);
|
||||
const [typeOfAddressForAutocomplete, setTypeOfAddressForAutocomplete] =
|
||||
useState<string | null>(null);
|
||||
|
||||
const { getPlaceAutocompleteData, getPlaceDetailsData } =
|
||||
useGetPlaceApiData();
|
||||
const { openDropdown } = useOpenDropdown();
|
||||
const { closeDropdown: closeDropdownHook } = useCloseDropdown();
|
||||
const { findCountryNameByCountryCode } = useCountryUtils();
|
||||
|
||||
const openDropdownOfAutocomplete = useCallback(() => {
|
||||
openDropdown({
|
||||
dropdownComponentInstanceIdFromProps:
|
||||
SELECT_AUTOCOMPLETE_LIST_DROPDOWN_ID,
|
||||
});
|
||||
}, [openDropdown]);
|
||||
|
||||
const closeDropdownOfAutocomplete = useCallback(() => {
|
||||
closeDropdownHook(SELECT_AUTOCOMPLETE_LIST_DROPDOWN_ID);
|
||||
setPlaceAutocompleteData(null);
|
||||
setTypeOfAddressForAutocomplete(null);
|
||||
}, [closeDropdownHook]);
|
||||
|
||||
const getAutocompletePlaceData = useDebouncedCallback(
|
||||
async (
|
||||
address: string,
|
||||
token: string,
|
||||
country?: string,
|
||||
isFieldCity?: boolean,
|
||||
) => {
|
||||
const placeAutocompleteData = await getPlaceAutocompleteData(
|
||||
address,
|
||||
token,
|
||||
country,
|
||||
isFieldCity,
|
||||
);
|
||||
|
||||
const newData = placeAutocompleteData?.map((data) => ({
|
||||
text: data.text,
|
||||
placeId: data.placeId,
|
||||
}));
|
||||
|
||||
if (isDefined(newData) && newData?.length > 0) {
|
||||
openDropdownOfAutocomplete();
|
||||
setPlaceAutocompleteData(newData);
|
||||
} else {
|
||||
closeDropdownOfAutocomplete();
|
||||
}
|
||||
},
|
||||
300,
|
||||
);
|
||||
|
||||
const autoFillInputsFromPlaceDetails = useCallback(
|
||||
async (
|
||||
placeId: string,
|
||||
token: string,
|
||||
addressStreet1?: string,
|
||||
internalValue?: FieldAddressDraftValue,
|
||||
) => {
|
||||
const placeData = await getPlaceDetailsData(placeId, token);
|
||||
const countryName = findCountryNameByCountryCode(placeData?.country);
|
||||
|
||||
const updatedAddress = {
|
||||
addressStreet1: addressStreet1 || (internalValue?.addressStreet1 ?? ''),
|
||||
addressStreet2: internalValue?.addressStreet2 ?? null,
|
||||
addressCity: placeData?.city || (internalValue?.addressCity ?? null),
|
||||
addressState: placeData?.state || (internalValue?.addressState ?? null),
|
||||
addressCountry: countryName || (internalValue?.addressCountry ?? null),
|
||||
addressPostcode:
|
||||
placeData?.postcode || (internalValue?.addressPostcode ?? null),
|
||||
addressLat:
|
||||
placeData?.location?.lat ?? internalValue?.addressLat ?? null,
|
||||
addressLng:
|
||||
placeData?.location?.lng ?? internalValue?.addressLng ?? null,
|
||||
};
|
||||
|
||||
setTokenForPlaceApi(null);
|
||||
closeDropdownOfAutocomplete();
|
||||
onChange?.(updatedAddress);
|
||||
|
||||
return updatedAddress;
|
||||
},
|
||||
[
|
||||
getPlaceDetailsData,
|
||||
findCountryNameByCountryCode,
|
||||
closeDropdownOfAutocomplete,
|
||||
onChange,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
placeAutocompleteData,
|
||||
tokenForPlaceApi,
|
||||
typeOfAddressForAutocomplete,
|
||||
setTokenForPlaceApi,
|
||||
setTypeOfAddressForAutocomplete,
|
||||
getAutocompletePlaceData,
|
||||
autoFillInputsFromPlaceDetails,
|
||||
closeDropdownOfAutocomplete,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user