Files
twenty/packages/twenty-front/src/modules/ui/field/input/hooks/useAddressAutocomplete.ts
T
Naifer 4eba13e9fb 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>
2025-07-29 09:56:08 +02:00

121 lines
4.0 KiB
TypeScript

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,
};
};