Phone country code unique (#9035)

fix #8775
This commit is contained in:
Guillim
2024-12-19 16:42:18 +01:00
committed by GitHub
parent 3f58a41d2f
commit 360c34fd18
47 changed files with 878 additions and 132 deletions

View File

@ -88,6 +88,7 @@ const mocks = [
phones {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
position
@ -95,6 +96,7 @@ const mocks = [
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
workPreference
@ -246,6 +248,7 @@ const mocks = [
phones {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
position
@ -253,6 +256,7 @@ const mocks = [
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
workPreference

View File

@ -246,6 +246,7 @@ mutation UpdateOneFavorite(
phones {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
position
@ -253,6 +254,7 @@ mutation UpdateOneFavorite(
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
workPreference
@ -532,6 +534,7 @@ export const mocks = [
phones {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
position
@ -539,6 +542,7 @@ export const mocks = [
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
workPreference

View File

@ -198,6 +198,7 @@ phone
{
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
}
linkedinLink
{

View File

@ -48,6 +48,7 @@ describe('mapObjectMetadataToGraphQLQuery', () => {
{
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
}
createdAt
avatarUrl

View File

@ -157,6 +157,7 @@ ${mapObjectMetadataToGraphQLQuery({
{
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}`;
}

View File

@ -30,6 +30,7 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS = `
phones {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
position
@ -37,6 +38,7 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS = `
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
workPreference
@ -229,6 +231,7 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS = `
phones {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
pointOfContactForOpportunities {
@ -305,6 +308,7 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS = `
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
workPreference

View File

@ -38,6 +38,7 @@ export const responseData = {
},
phones: {
primaryPhoneCountryCode: '',
primaryPhoneCallingCode: '',
primaryPhoneNumber: '',
},
linkedinLink: {

View File

@ -43,6 +43,7 @@ export const responseData = {
},
phones: {
primaryPhoneCountryCode: '',
primaryPhoneCallingCode: '',
primaryPhoneNumber: '',
},
linkedinLink: {

View File

@ -178,6 +178,7 @@ const mocks: MockedResponse[] = [
phones {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
position
@ -185,6 +186,7 @@ const mocks: MockedResponse[] = [
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
workPreference
@ -332,6 +334,7 @@ const mocks: MockedResponse[] = [
phones {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
position
@ -339,6 +342,7 @@ const mocks: MockedResponse[] = [
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
workPreference

View File

@ -39,7 +39,8 @@ const mocks: MockedResponse[] = [
input: {
phones: {
primaryPhoneNumber: '123 456',
primaryPhoneCountryCode: '+1',
primaryPhoneCountryCode: 'US',
primaryPhoneCallingCode: '+1',
additionalPhones: [],
},
},
@ -134,7 +135,8 @@ describe('usePersistField', () => {
act(() => {
result.current.persistField({
primaryPhoneNumber: '123 456',
primaryPhoneCountryCode: '+1',
primaryPhoneCountryCode: 'US',
primaryPhoneCallingCode: '+1',
additionalPhones: [],
});
});

View File

@ -208,6 +208,7 @@ const mocks: MockedResponse[] = [
phones {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
position
@ -215,6 +216,7 @@ const mocks: MockedResponse[] = [
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
workPreference

View File

@ -9,12 +9,11 @@ import { TEXT_INPUT_STYLE } from 'twenty-ui';
import { MultiItemFieldInput } from './MultiItemFieldInput';
import { createPhonesFromFieldValue } from '@/object-record/record-field/meta-types/input/utils/phonesUtils';
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
import { PhoneCountryPickerDropdownButton } from '@/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
export const DEFAULT_PHONE_COUNTRY_CODE = '1';
export const DEFAULT_PHONE_CALLING_CODE = '1';
const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)`
font-family: ${({ theme }) => theme.font.family};
@ -60,22 +59,22 @@ export const PhonesFieldInput = ({
const phones = createPhonesFromFieldValue(fieldValue);
const defaultCallingCode =
stripSimpleQuotesFromString(
fieldDefinition?.defaultValue?.primaryPhoneCountryCode,
) ?? DEFAULT_PHONE_COUNTRY_CODE;
// TODO : improve once we store the real country code
const defaultCountry = useCountries().find(
(obj) => `+${obj.callingCode}` === defaultCallingCode,
)?.countryCode;
const defaultCountry = stripSimpleQuotesFromString(
fieldDefinition?.defaultValue?.primaryPhoneCountryCode,
);
const handlePersistPhones = (
updatedPhones: { number: string; callingCode: string }[],
updatedPhones: {
number: string;
countryCode: string;
callingCode: string;
}[],
) => {
const [nextPrimaryPhone, ...nextAdditionalPhones] = updatedPhones;
persistPhonesField({
primaryPhoneNumber: nextPrimaryPhone?.number ?? '',
primaryPhoneCountryCode: nextPrimaryPhone?.callingCode ?? '',
primaryPhoneCountryCode: nextPrimaryPhone?.countryCode ?? '',
primaryPhoneCallingCode: nextPrimaryPhone?.callingCode ?? '',
additionalPhones: nextAdditionalPhones,
});
};
@ -96,11 +95,13 @@ export const PhonesFieldInput = ({
return {
number: phone.nationalNumber,
callingCode: `+${phone.countryCallingCode}`,
countryCode: phone.country as string,
};
}
return {
number: '',
callingCode: '',
countryCode: '',
};
}}
renderItem={({

View File

@ -19,7 +19,8 @@ describe('createPhonesFromFieldValue test suite', () => {
it('should return an array with primary phone number if it is defined', () => {
const fieldValue: FieldPhonesValue = {
primaryPhoneNumber: '123456789',
primaryPhoneCountryCode: '+1',
primaryPhoneCountryCode: 'US',
primaryPhoneCallingCode: '+1',
additionalPhones: [],
};
const result = createPhonesFromFieldValue(fieldValue);
@ -27,6 +28,24 @@ describe('createPhonesFromFieldValue test suite', () => {
{
number: '123456789',
callingCode: '+1',
countryCode: 'US',
},
]);
});
it('should return an array with primary phone number if it is defined, even with incorrect callingCode', () => {
const fieldValue: FieldPhonesValue = {
primaryPhoneNumber: '123456789',
primaryPhoneCountryCode: 'US',
primaryPhoneCallingCode: '+33',
additionalPhones: [],
};
const result = createPhonesFromFieldValue(fieldValue);
expect(result).toEqual([
{
number: '123456789',
callingCode: '+33',
countryCode: 'US',
},
]);
});
@ -34,10 +53,11 @@ describe('createPhonesFromFieldValue test suite', () => {
it('should return an array with both primary and additional phones if they are defined', () => {
const fieldValue: FieldPhonesValue = {
primaryPhoneNumber: '123456789',
primaryPhoneCountryCode: '+1',
primaryPhoneCountryCode: 'US',
primaryPhoneCallingCode: '+1',
additionalPhones: [
{ number: '987654321', callingCode: '+44' },
{ number: '555555555', callingCode: '+33' },
{ number: '987654321', callingCode: '+44', countryCode: 'GB' },
{ number: '555555555', callingCode: '+33', countryCode: 'FR' },
],
};
const result = createPhonesFromFieldValue(fieldValue);
@ -45,9 +65,10 @@ describe('createPhonesFromFieldValue test suite', () => {
{
number: '123456789',
callingCode: '+1',
countryCode: 'US',
},
{ number: '987654321', callingCode: '+44' },
{ number: '555555555', callingCode: '+33' },
{ number: '987654321', callingCode: '+44', countryCode: 'GB' },
{ number: '555555555', callingCode: '+33', countryCode: 'FR' },
]);
});
@ -56,14 +77,14 @@ describe('createPhonesFromFieldValue test suite', () => {
primaryPhoneNumber: '',
primaryPhoneCountryCode: '',
additionalPhones: [
{ number: '987654321', callingCode: '+44' },
{ number: '555555555', callingCode: '+33' },
{ number: '987654321', callingCode: '+44', countryCode: 'GB' },
{ number: '555555555', callingCode: '+33', countryCode: 'FR' },
],
};
const result = createPhonesFromFieldValue(fieldValue);
expect(result).toEqual([
{ number: '987654321', callingCode: '+44' },
{ number: '555555555', callingCode: '+33' },
{ number: '987654321', callingCode: '+44', countryCode: 'GB' },
{ number: '555555555', callingCode: '+33', countryCode: 'FR' },
]);
});
@ -72,22 +93,34 @@ describe('createPhonesFromFieldValue test suite', () => {
primaryPhoneNumber: ' ',
primaryPhoneCountryCode: '',
additionalPhones: [
{ number: '987654321', callingCode: '+44' },
{ number: '555555555', callingCode: '+33' },
{ number: '987654321', callingCode: '+44', countryCode: 'GB' },
{ number: '555555555', callingCode: '+33', countryCode: 'FR' },
],
};
const result = createPhonesFromFieldValue(fieldValue);
expect(result).toEqual([
{ number: ' ', callingCode: '' },
{ number: '987654321', callingCode: '+44' },
{ number: '555555555', callingCode: '+33' },
{ number: ' ', callingCode: '', countryCode: '' },
{ number: '987654321', callingCode: '+44', countryCode: 'GB' },
{ number: '555555555', callingCode: '+33', countryCode: 'FR' },
]);
});
it('should return an empty array if only country code is defined', () => {
it('should return an empty array if only country and calling code are defined', () => {
const fieldValue: FieldPhonesValue = {
primaryPhoneNumber: '',
primaryPhoneCountryCode: '+33',
primaryPhoneCountryCode: 'FR',
primaryPhoneCallingCode: '+33',
additionalPhones: [],
};
const result = createPhonesFromFieldValue(fieldValue);
expect(result).toEqual([]);
});
it('should return an empty array if only calling code is defined', () => {
const fieldValue: FieldPhonesValue = {
primaryPhoneNumber: '',
primaryPhoneCallingCode: '+33',
primaryPhoneCountryCode: '',
additionalPhones: [],
};
const result = createPhonesFromFieldValue(fieldValue);

View File

@ -8,7 +8,10 @@ export const createPhonesFromFieldValue = (fieldValue: FieldPhonesValue) => {
fieldValue.primaryPhoneNumber
? {
number: fieldValue.primaryPhoneNumber,
callingCode: fieldValue.primaryPhoneCountryCode,
callingCode: fieldValue.primaryPhoneCallingCode
? fieldValue.primaryPhoneCallingCode
: fieldValue.primaryPhoneCountryCode,
countryCode: fieldValue.primaryPhoneCountryCode,
}
: null,
...(fieldValue.additionalPhones ?? []),

View File

@ -27,6 +27,7 @@ export type FieldDateTimeDraftValue = string;
export type FieldPhonesDraftValue = {
primaryPhoneNumber: string;
primaryPhoneCountryCode: string;
primaryPhoneCallingCode: string;
additionalPhones?: PhoneRecord[] | null;
};
export type FieldEmailsDraftValue = {

View File

@ -265,10 +265,15 @@ export type FieldActorValue = {
export type FieldArrayValue = string[];
export type PhoneRecord = { number: string; callingCode: string };
export type PhoneRecord = {
number: string;
callingCode: string;
countryCode: string;
};
export type FieldPhonesValue = {
primaryPhoneNumber: string;
primaryPhoneCountryCode: string;
primaryPhoneCallingCode?: string;
additionalPhones?: PhoneRecord[] | null;
};

View File

@ -5,8 +5,15 @@ import { FieldPhonesValue } from '../FieldMetadata';
export const phonesSchema = z.object({
primaryPhoneNumber: z.string(),
primaryPhoneCountryCode: z.string(),
primaryPhoneCallingCode: z.string(),
additionalPhones: z
.array(z.object({ number: z.string(), callingCode: z.string() }))
.array(
z.object({
number: z.string(),
callingCode: z.string(),
countryCode: z.string(),
}),
)
.nullable(),
}) satisfies z.ZodType<FieldPhonesValue>;

View File

@ -71,6 +71,9 @@ export const computeDraftValueFromFieldValue = <FieldValue>({
primaryPhoneCountryCode: stripSimpleQuotesFromString(
fieldDefinition?.defaultValue?.primaryPhoneCountryCode,
),
primaryPhoneCallingCode: stripSimpleQuotesFromString(
fieldDefinition?.defaultValue?.primaryPhoneCallingCode,
),
} as unknown as FieldInputDraftValue<FieldValue>;
}

View File

@ -21,6 +21,7 @@ const mockPerson = {
whatsapp: {
primaryPhoneNumber: '+1',
primaryPhoneCountryCode: '234-567-890',
primaryPhoneCallingCode: '+33',
additionalPhones: [],
},
linkedinLink: {

View File

@ -663,7 +663,8 @@ export const mockPerformance = {
id: '20202020-2d40-4e49-8df4-9c6a049191df',
email: 'lorie.vladim@google.com',
phones: {
primaryPhoneCountryCode: '+33',
primaryPhoneCountryCode: 'FR',
primaryPhoneCallingCode: '+33',
primaryPhoneNumber: '788901235',
},
linkedinLink: {

View File

@ -207,6 +207,7 @@ const companyMocks = [
phones {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
position
@ -214,6 +215,7 @@ const companyMocks = [
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
workPreference

View File

@ -95,6 +95,7 @@ export const generateEmptyFieldValue = (
return {
primaryPhoneNumber: '',
primaryPhoneCountryCode: '',
primaryPhoneCallingCode: '',
additionalPhones: null,
};
}

View File

@ -91,7 +91,9 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
exampleValue: {
primaryPhoneNumber: '234-567-890',
primaryPhoneCountryCode: '+1',
additionalPhones: [{ number: '234-567-890', callingCode: '+1' }],
additionalPhones: [
{ number: '234-567-890', callingCode: '+1', countryCode: 'US' },
],
},
subFields: [
'primaryPhoneNumber',
@ -102,6 +104,7 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
labelBySubField: {
primaryPhoneNumber: 'Primary Phone Number',
primaryPhoneCountryCode: 'Primary Phone Country Code',
primaryPhoneCallingCode: 'Primary Phone Calling Code',
additionalPhones: 'Additional Phones',
},
category: 'Basic',

View File

@ -3,8 +3,10 @@ import { Controller, useFormContext } from 'react-hook-form';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { phonesSchema as phonesFieldDefaultValueSchema } from '@/object-record/record-field/types/guards/isFieldPhonesValue';
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
import { countryCodeToCallingCode } from '@/settings/data-model/fields/preview/utils/getPhonesFieldPreviewValue';
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
import { Select } from '@/ui/input/components/Select';
import { CountryCode } from 'libphonenumber-js';
import { IconMap } from 'twenty-ui';
import { z } from 'zod';
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
@ -27,22 +29,27 @@ export type SettingsDataModelFieldTextFormValues = z.infer<
typeof settingsDataModelFieldPhonesFormSchema
>;
export type CountryCodeOrEmpty = CountryCode | '';
export const SettingsDataModelFieldPhonesForm = ({
disabled,
fieldMetadataItem,
}: SettingsDataModelFieldPhonesFormProps) => {
const { control } = useFormContext<SettingsDataModelFieldTextFormValues>();
const countries = useCountries()
.sort((a, b) => a.countryName.localeCompare(b.countryName))
.map((country) => ({
label: `${country.countryName} (+${country.callingCode})`,
value: `+${country.callingCode}`,
}));
countries.unshift({ label: 'No country', value: '' });
const countries = [
{ label: 'No country', value: '' },
...useCountries()
.sort((a, b) => a.countryName.localeCompare(b.countryName))
.map((country) => ({
label: `${country.countryName} (+${country.callingCode})`,
value: country.countryCode as CountryCodeOrEmpty,
})),
];
const defaultDefaultValue = {
primaryPhoneNumber: "''",
primaryPhoneCountryCode: "''",
primaryPhoneCallingCode: "''",
additionalPhones: null,
};
const fieldMetadataItemDefaultValue = fieldMetadataItem?.defaultValue;
@ -73,6 +80,9 @@ export const SettingsDataModelFieldPhonesForm = ({
...value,
primaryPhoneCountryCode:
applySimpleQuotesToString(newPhoneCountryCode),
primaryPhoneCallingCode: applySimpleQuotesToString(
countryCodeToCallingCode(newPhoneCountryCode),
),
})
}
disabled={disabled}

View File

@ -1,9 +1,29 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { DEFAULT_PHONE_CALLING_CODE } from '@/object-record/record-field/meta-types/input/components/PhonesFieldInput';
import { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata';
import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig';
import {
CountryCode,
getCountries,
getCountryCallingCode,
} from 'libphonenumber-js';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
const isStrCountryCodeGuard = (str: string): str is CountryCode => {
return getCountries().includes(str as CountryCode);
};
export const countryCodeToCallingCode = (countryCode: string): string => {
if (!countryCode || !isStrCountryCodeGuard(countryCode)) {
return `+${DEFAULT_PHONE_CALLING_CODE}`;
}
const callingCode = getCountryCallingCode(countryCode);
return callingCode ? `+${callingCode}` : `+${DEFAULT_PHONE_CALLING_CODE}`;
};
export const getPhonesFieldPreviewValue = ({
fieldMetadataItem,
}: {
@ -26,8 +46,16 @@ export const getPhonesFieldPreviewValue = ({
fieldMetadataItem.defaultValue?.primaryPhoneCountryCode,
)
: null;
const primaryPhoneCallingCode =
fieldMetadataItem.defaultValue?.primaryPhoneCallingCode &&
fieldMetadataItem.defaultValue.primaryPhoneCallingCode !== ''
? stripSimpleQuotesFromString(
fieldMetadataItem.defaultValue?.primaryPhoneCallingCode,
)
: null;
return {
...placeholderDefaultValue,
primaryPhoneCountryCode,
primaryPhoneCallingCode,
};
};

View File

@ -5,6 +5,7 @@ import { RoundedLink, THEME_COMMON } from 'twenty-ui';
import { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
import { DEFAULT_PHONE_CALLING_CODE } from '@/object-record/record-field/meta-types/input/components/PhonesFieldInput';
import { parsePhoneNumber } from 'libphonenumber-js';
import { isDefined } from '~/utils/isDefined';
import { logError } from '~/utils/logError';
@ -36,7 +37,10 @@ export const PhonesDisplay = ({ value, isFocused }: PhonesDisplayProps) => {
value?.primaryPhoneNumber
? {
number: value.primaryPhoneNumber,
callingCode: value.primaryPhoneCountryCode,
callingCode:
value.primaryPhoneCallingCode ||
value.primaryPhoneCountryCode ||
`+${DEFAULT_PHONE_CALLING_CODE}`,
}
: null,
...parseAdditionalPhones(value?.additionalPhones),
@ -50,11 +54,11 @@ export const PhonesDisplay = ({ value, isFocused }: PhonesDisplayProps) => {
}),
[
value?.primaryPhoneNumber,
value?.primaryPhoneCallingCode,
value?.primaryPhoneCountryCode,
value?.additionalPhones,
],
);
const parsePhoneNumberOrReturnInvalidValue = (number: string) => {
try {
return { parsedPhone: parsePhoneNumber(number) };