diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormAddressFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormAddressFieldInput.tsx index 72f0c753f4..2d1aa28505 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormAddressFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormAddressFieldInput.tsx @@ -7,6 +7,7 @@ import { type FieldAddressDraftValue } from '@/object-record/record-field/ui/typ import { type FieldAddressValue } from '@/object-record/record-field/ui/types/FieldMetadata'; import { InputLabel } from '@/ui/input/components/InputLabel'; import { t } from '@lingui/core/macro'; +import { normalizeAddressFieldValueForPersist } from '~/utils/normalize-address-field-value-for-persist'; type FormAddressFieldInputProps = { label?: string; @@ -36,7 +37,7 @@ export const FormAddressFieldInput = ({ addressLng: defaultValue?.addressLng ?? null, [field]: updatedAddressPart, }; - onChange(updatedAddress); + onChange(normalizeAddressFieldValueForPersist(updatedAddress)); }; return ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/AddressFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/AddressFieldInput.tsx index 625f0a54b4..a61411c0ae 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/AddressFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/AddressFieldInput.tsx @@ -8,6 +8,7 @@ import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useContext } from 'react'; import { castAsNumberOrNull } from '~/utils/cast-as-number-or-null'; +import { normalizeAddressFieldValueForPersist } from '~/utils/normalize-address-field-value-for-persist'; export const AddressFieldInput = () => { const { draftValue, setDraftValue, fieldDefinition } = useAddressField(); @@ -19,7 +20,7 @@ export const AddressFieldInput = () => { const convertToAddress = ( newAddress: FieldAddressDraftValue | undefined, ): FieldAddressDraftValue => { - return { + return normalizeAddressFieldValueForPersist({ addressStreet1: newAddress?.addressStreet1 ?? '', addressStreet2: newAddress?.addressStreet2 ?? null, addressCity: newAddress?.addressCity ?? null, @@ -28,7 +29,7 @@ export const AddressFieldInput = () => { addressPostcode: newAddress?.addressPostcode ?? null, addressLat: castAsNumberOrNull(newAddress?.addressLat), addressLng: castAsNumberOrNull(newAddress?.addressLng), - }; + }); }; const settings = fieldDefinition.metadata.settings; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/utils/computeEmptyDraftValue.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/utils/computeEmptyDraftValue.ts index d34ec91196..188d5943c4 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/utils/computeEmptyDraftValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/utils/computeEmptyDraftValue.ts @@ -48,6 +48,8 @@ export const computeEmptyDraftValue = ({ addressState: '', addressCountry: '', addressPostcode: '', + addressLat: null, + addressLng: null, } as unknown as FieldInputDraftValue; } diff --git a/packages/twenty-front/src/utils/__tests__/normalize-address-field-value-for-persist.test.ts b/packages/twenty-front/src/utils/__tests__/normalize-address-field-value-for-persist.test.ts new file mode 100644 index 0000000000..193f7fe6ce --- /dev/null +++ b/packages/twenty-front/src/utils/__tests__/normalize-address-field-value-for-persist.test.ts @@ -0,0 +1,40 @@ +import { type FieldAddressValue } from '@/object-record/record-field/ui/types/FieldMetadata'; +import { normalizeAddressFieldValueForPersist } from '~/utils/normalize-address-field-value-for-persist'; + +describe('normalizeAddressFieldValueForPersist', () => { + it('should set coordinates to null when all text subfields are empty', () => { + const input: FieldAddressValue = { + addressStreet1: '', + addressStreet2: null, + addressCity: null, + addressState: null, + addressPostcode: null, + addressCountry: null, + addressLat: 40.7128, + addressLng: -74.006, + }; + + const result = normalizeAddressFieldValueForPersist(input); + + expect(result.addressLat).toBeNull(); + expect(result.addressLng).toBeNull(); + }); + + it('should preserve coordinates when any text subfield is non-empty', () => { + const input: FieldAddressValue = { + addressStreet1: '1600 Amphitheatre', + addressStreet2: null, + addressCity: null, + addressState: null, + addressPostcode: null, + addressCountry: null, + addressLat: 37.422, + addressLng: -122.084, + }; + + const result = normalizeAddressFieldValueForPersist(input); + + expect(result.addressLat).toBe(37.422); + expect(result.addressLng).toBe(-122.084); + }); +}); diff --git a/packages/twenty-front/src/utils/normalize-address-field-value-for-persist.ts b/packages/twenty-front/src/utils/normalize-address-field-value-for-persist.ts new file mode 100644 index 0000000000..38e041d402 --- /dev/null +++ b/packages/twenty-front/src/utils/normalize-address-field-value-for-persist.ts @@ -0,0 +1,28 @@ +import { isNonEmptyString } from '@sniptt/guards'; + +import { type FieldAddressValue } from '@/object-record/record-field/ui/types/FieldMetadata'; + +// When every user-editable text part of the address is empty, clear coordinates +// so DB columns for addressLat/addressLng are updated (see formatCompositeField +// in twenty-server: omitted keys are not written, leaving old coordinates in place). +export const normalizeAddressFieldValueForPersist = ( + address: FieldAddressValue, +): FieldAddressValue => { + const allTextPartsEmpty = + !isNonEmptyString(address.addressStreet1) && + !isNonEmptyString(address.addressStreet2) && + !isNonEmptyString(address.addressCity) && + !isNonEmptyString(address.addressState) && + !isNonEmptyString(address.addressPostcode) && + !isNonEmptyString(address.addressCountry); + + if (!allTextPartsEmpty) { + return address; + } + + return { + ...address, + addressLat: null, + addressLng: null, + }; +};