diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useRightDrawerEmailThread.test.tsx b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useRightDrawerEmailThread.test.tsx index 0af5cec8224a..f71f7c40dc35 100644 --- a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useRightDrawerEmailThread.test.tsx +++ b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useRightDrawerEmailThread.test.tsx @@ -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 diff --git a/packages/twenty-front/src/modules/favorites/hooks/__mocks__/useFavorites.ts b/packages/twenty-front/src/modules/favorites/hooks/__mocks__/useFavorites.ts index 154bad46cf6d..50c194f4332f 100644 --- a/packages/twenty-front/src/modules/favorites/hooks/__mocks__/useFavorites.ts +++ b/packages/twenty-front/src/modules/favorites/hooks/__mocks__/useFavorites.ts @@ -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 diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.ts b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.ts index 7208246e7e49..6a41f42f7173 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.ts @@ -198,6 +198,7 @@ phone { primaryPhoneNumber primaryPhoneCountryCode + primaryPhoneCallingCode } linkedinLink { diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.ts b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.ts index d2650b69807f..1f39e7a59ee9 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.ts @@ -48,6 +48,7 @@ describe('mapObjectMetadataToGraphQLQuery', () => { { primaryPhoneNumber primaryPhoneCountryCode + primaryPhoneCallingCode } createdAt avatarUrl diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts index bf29d99ee1f9..4cd8dbcbb009 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts @@ -157,6 +157,7 @@ ${mapObjectMetadataToGraphQLQuery({ { primaryPhoneNumber primaryPhoneCountryCode + primaryPhoneCallingCode additionalPhones }`; } diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/personFragments.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/personFragments.ts index 97077eaacf43..59c877457f40 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/personFragments.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/personFragments.ts @@ -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 diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useCreateManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useCreateManyRecords.ts index 4202b495c6ea..9edba4dd4a0e 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useCreateManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useCreateManyRecords.ts @@ -38,6 +38,7 @@ export const responseData = { }, phones: { primaryPhoneCountryCode: '', + primaryPhoneCallingCode: '', primaryPhoneNumber: '', }, linkedinLink: { diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useCreateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useCreateOneRecord.ts index 00522c295e47..1477530b90db 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useCreateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useCreateOneRecord.ts @@ -43,6 +43,7 @@ export const responseData = { }, phones: { primaryPhoneCountryCode: '', + primaryPhoneCallingCode: '', primaryPhoneNumber: '', }, linkedinLink: { diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useLazyLoadRecordIndexTable.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useLazyLoadRecordIndexTable.test.tsx index a6232fefa52d..4cebe899e0db 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useLazyLoadRecordIndexTable.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useLazyLoadRecordIndexTable.test.tsx @@ -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 diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx index 4eea3aae834f..be8d16d1aa34 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx @@ -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: [], }); }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx index 79b1a27166b6..7149e7c8016c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx @@ -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 diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/PhonesFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/PhonesFieldInput.tsx index 58d8efe5ffd9..cca5d9b8a362 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/PhonesFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/PhonesFieldInput.tsx @@ -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={({ diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/utils/__tests__/phonesUtils.test.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/utils/__tests__/phonesUtils.test.ts index 11f28a19b8b6..ddb7075bc498 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/utils/__tests__/phonesUtils.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/utils/__tests__/phonesUtils.test.ts @@ -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: '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: '', - primaryPhoneCountryCode: '+33', + primaryPhoneCallingCode: '+33', + primaryPhoneCountryCode: '', additionalPhones: [], }; const result = createPhonesFromFieldValue(fieldValue); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/utils/phonesUtils.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/utils/phonesUtils.ts index 56f413d8aac2..b9e5db952640 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/utils/phonesUtils.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/utils/phonesUtils.ts @@ -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 ?? []), diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts index 6bd2ee66810b..9d8bf3105f3c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts @@ -27,6 +27,7 @@ export type FieldDateTimeDraftValue = string; export type FieldPhonesDraftValue = { primaryPhoneNumber: string; primaryPhoneCountryCode: string; + primaryPhoneCallingCode: string; additionalPhones?: PhoneRecord[] | null; }; export type FieldEmailsDraftValue = { diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts index 0e0875b5029f..f61159ac27f0 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts @@ -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; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldPhonesValue.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldPhonesValue.ts index 90cb812da927..178240825a9a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldPhonesValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldPhonesValue.ts @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromFieldValue.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromFieldValue.ts index a22e42259acd..4e5b407f2cd6 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromFieldValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromFieldValue.ts @@ -71,6 +71,9 @@ export const computeDraftValueFromFieldValue = ({ primaryPhoneCountryCode: stripSimpleQuotesFromString( fieldDefinition?.defaultValue?.primaryPhoneCountryCode, ), + primaryPhoneCallingCode: stripSimpleQuotesFromString( + fieldDefinition?.defaultValue?.primaryPhoneCallingCode, + ), } as unknown as FieldInputDraftValue; } diff --git a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useExportFetchRecords.test.ts b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useExportFetchRecords.test.ts index 21e9775c0d9f..807f15f58e09 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useExportFetchRecords.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useExportFetchRecords.test.ts @@ -21,6 +21,7 @@ const mockPerson = { whatsapp: { primaryPhoneNumber: '+1', primaryPhoneCountryCode: '234-567-890', + primaryPhoneCallingCode: '+33', additionalPhones: [], }, linkedinLink: { diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/mock.ts b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/mock.ts index ce0253ddab59..7e828037b187 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/mock.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/mock.ts @@ -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: { diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useOpenObjectRecordsSpreadsheetImportDialog.test.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useOpenObjectRecordsSpreadsheetImportDialog.test.ts index f52e401cbc10..3327365b86cd 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useOpenObjectRecordsSpreadsheetImportDialog.test.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useOpenObjectRecordsSpreadsheetImportDialog.test.ts @@ -207,6 +207,7 @@ const companyMocks = [ phones { primaryPhoneNumber primaryPhoneCountryCode + primaryPhoneCallingCode additionalPhones } position @@ -214,6 +215,7 @@ const companyMocks = [ whatsapp { primaryPhoneNumber primaryPhoneCountryCode + primaryPhoneCallingCode additionalPhones } workPreference diff --git a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts index 4d4a651380e8..7b1059b15eeb 100644 --- a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts +++ b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts @@ -95,6 +95,7 @@ export const generateEmptyFieldValue = ( return { primaryPhoneNumber: '', primaryPhoneCountryCode: '', + primaryPhoneCallingCode: '', additionalPhones: null, }; } diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts index cd6c9a8b42b9..294dd35d7504 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts @@ -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', diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm.tsx index 070aa79cfead..f0a3d59e1413 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm.tsx @@ -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(); - 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} diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getPhonesFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getPhonesFieldPreviewValue.ts index 3cd113cd98ac..8e8c7bc053ca 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getPhonesFieldPreviewValue.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getPhonesFieldPreviewValue.ts @@ -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, }; }; diff --git a/packages/twenty-front/src/modules/ui/field/display/components/PhonesDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/PhonesDisplay.tsx index de23dbc0878a..9b3e4c5e06a4 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/PhonesDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/PhonesDisplay.tsx @@ -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) }; diff --git a/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts b/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts index 6a48fa714839..76e898dd3d0e 100644 --- a/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts +++ b/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts @@ -19461,7 +19461,8 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "defaultValue": { "additionalPhones": null, "primaryPhoneNumber": "''", - "primaryPhoneCountryCode": "''" + "primaryPhoneCountryCode": "''", + "primaryPhoneCallingCode": "''" }, "options": null, "isLabelSyncedWithName": false, @@ -19740,7 +19741,8 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = { "additionalPhones": {}, "primaryPhoneNumber": "", - "primaryPhoneCountryCode": "" + "primaryPhoneCountryCode": "", + "primaryPhoneCallingCode": "" } ], "options": null, diff --git a/packages/twenty-front/src/testing/mock-data/people.ts b/packages/twenty-front/src/testing/mock-data/people.ts index b764118afda1..f09fc18aff47 100644 --- a/packages/twenty-front/src/testing/mock-data/people.ts +++ b/packages/twenty-front/src/testing/mock-data/people.ts @@ -47,7 +47,8 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { city: 'ASd', phones: { primaryPhoneNumber: '781234562', - primaryPhoneCountryCode: '+33', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', }, id: 'da3c2c4b-da01-4b81-9734-226069eb4cd0', jobTitle: '', @@ -177,7 +178,8 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { city: 'Seattle', phones: { primaryPhoneNumber: '781234562', - primaryPhoneCountryCode: '+33', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', }, id: '20202020-1c0e-494c-a1b6-85b1c6fefaa5', jobTitle: '', @@ -307,7 +309,8 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { city: 'Los Angeles', phones: { primaryPhoneNumber: '781234576', - primaryPhoneCountryCode: '+33', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', }, id: '20202020-ac73-4797-824e-87a1f5aea9e0', jobTitle: '', @@ -406,7 +409,8 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { city: 'Seattle', phones: { primaryPhoneNumber: '781234545', - primaryPhoneCountryCode: '+33', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', }, id: '20202020-f517-42fd-80ae-14173b3b70ae', jobTitle: '', @@ -505,7 +509,8 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { city: 'Los Angeles', phones: { primaryPhoneNumber: '781234587', - primaryPhoneCountryCode: '+33', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', }, id: '20202020-eee1-4690-ad2c-8619e5b56a2e', jobTitle: '', @@ -604,7 +609,8 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { city: 'Seattle', phones: { primaryPhoneNumber: '781234599', - primaryPhoneCountryCode: '+33', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', }, id: '20202020-6784-4449-afdf-dc62cb8702f2', jobTitle: '', @@ -703,7 +709,8 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { city: 'New York', phones: { primaryPhoneNumber: '781234572', - primaryPhoneCountryCode: '+33', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', }, id: '20202020-490f-4466-8391-733cfd66a0c8', jobTitle: '', @@ -802,7 +809,8 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { city: 'Seattle', phones: { primaryPhoneNumber: '781234582', - primaryPhoneCountryCode: '+33', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', }, id: '20202020-80f1-4dff-b570-a74942528de3', jobTitle: '', @@ -901,7 +909,8 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { city: 'New York', phones: { primaryPhoneNumber: '781234569', - primaryPhoneCountryCode: '+33', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', }, id: '20202020-338b-46df-8811-fa08c7d19d35', jobTitle: '', @@ -1000,7 +1009,8 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { city: 'San Francisco', phones: { primaryPhoneNumber: '781234962', - primaryPhoneCountryCode: '+33', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', }, id: '20202020-64ad-4b0e-bbfd-e9fd795b7016', jobTitle: '', @@ -1099,7 +1109,8 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { city: 'New York', phones: { primaryPhoneNumber: '781234502', - primaryPhoneCountryCode: '+33', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', }, id: '20202020-5d54-41b7-ba36-f0d20e1417ae', jobTitle: '', @@ -1198,7 +1209,8 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { city: 'Los Angeles', phones: { primaryPhoneNumber: '781234563', - primaryPhoneCountryCode: '+33', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', }, id: '20202020-623d-41fe-92e7-dd45b7c568e1', jobTitle: '', @@ -1297,7 +1309,8 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { city: 'Seattle', phones: { primaryPhoneNumber: '781234542', - primaryPhoneCountryCode: '+33', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', }, id: '20202020-2d40-4e49-8df4-9c6a049190ef', jobTitle: '', @@ -1396,7 +1409,8 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { city: 'Seattle', phones: { primaryPhoneNumber: '782234562', - primaryPhoneCountryCode: '+33', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', }, id: '20202020-2d40-4e49-8df4-9c6a049190df', jobTitle: '', @@ -1495,7 +1509,8 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { city: 'Seattle', phones: { primaryPhoneNumber: '781274562', - primaryPhoneCountryCode: '+33', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', }, id: '20202020-2d40-4e49-8df4-9c6a049191de', jobTitle: '', @@ -1594,7 +1609,8 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { city: 'Seattle', phones: { primaryPhoneNumber: '781239562', - primaryPhoneCountryCode: '+33', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', }, id: '20202020-2d40-4e49-8df4-9c6a049191df', jobTitle: '', diff --git a/packages/twenty-server/src/database/commands/active-workspaces.command.ts b/packages/twenty-server/src/database/commands/active-workspaces.command.ts index d741144cb8ea..46c7ad2201b9 100644 --- a/packages/twenty-server/src/database/commands/active-workspaces.command.ts +++ b/packages/twenty-server/src/database/commands/active-workspaces.command.ts @@ -1,5 +1,3 @@ -import { Logger } from '@nestjs/common'; - import chalk from 'chalk'; import { Option } from 'nest-commander'; import { Repository } from 'typeorm'; @@ -20,11 +18,8 @@ export type ActiveWorkspacesCommandOptions = BaseCommandOptions & { export abstract class ActiveWorkspacesCommandRunner extends BaseCommandRunner { private workspaceIds: string[] = []; - protected readonly logger: Logger; - constructor(protected readonly workspaceRepository: Repository) { super(); - this.logger = new Logger(this.constructor.name); } @Option({ diff --git a/packages/twenty-server/src/database/commands/base.command.ts b/packages/twenty-server/src/database/commands/base.command.ts index 419a6f114b2e..780eb18c1eb4 100644 --- a/packages/twenty-server/src/database/commands/base.command.ts +++ b/packages/twenty-server/src/database/commands/base.command.ts @@ -3,6 +3,7 @@ import { Logger } from '@nestjs/common'; import chalk from 'chalk'; import { CommandRunner, Option } from 'nest-commander'; +import { CommandLogger } from './logger'; export type BaseCommandOptions = { workspaceId?: string; dryRun?: boolean; @@ -10,11 +11,13 @@ export type BaseCommandOptions = { }; export abstract class BaseCommandRunner extends CommandRunner { - protected readonly logger: Logger; - + protected logger: CommandLogger | Logger; constructor() { super(); - this.logger = new Logger(this.constructor.name); + this.logger = new CommandLogger({ + verbose: false, + constructorName: this.constructor.name, + }); } @Option({ @@ -27,10 +30,11 @@ export abstract class BaseCommandRunner extends CommandRunner { } @Option({ - flags: '--verbose', + flags: '-v, --verbose', description: 'Verbose output', + required: false, }) - parseVerbose() { + parseVerbose(): boolean { return true; } @@ -38,6 +42,13 @@ export abstract class BaseCommandRunner extends CommandRunner { passedParams: string[], options: BaseCommandOptions, ): Promise { + if (options.verbose) { + this.logger = new CommandLogger({ + verbose: true, + constructorName: this.constructor.name, + }); + } + try { await this.executeBaseCommand(passedParams, options); } catch (error) { diff --git a/packages/twenty-server/src/database/commands/database-command.module.ts b/packages/twenty-server/src/database/commands/database-command.module.ts index 9c69bb3eb275..1ab5b849f46b 100644 --- a/packages/twenty-server/src/database/commands/database-command.module.ts +++ b/packages/twenty-server/src/database/commands/database-command.module.ts @@ -52,8 +52,8 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp UpgradeTo0_32CommandModule, UpgradeTo0_33CommandModule, UpgradeTo0_34CommandModule, - FeatureFlagModule, UpgradeTo0_40CommandModule, + FeatureFlagModule, ], providers: [ DataSeedWorkspaceCommand, diff --git a/packages/twenty-server/src/database/commands/logger.ts b/packages/twenty-server/src/database/commands/logger.ts new file mode 100644 index 000000000000..9bd2ebb02011 --- /dev/null +++ b/packages/twenty-server/src/database/commands/logger.ts @@ -0,0 +1,46 @@ +import { Logger } from '@nestjs/common'; + +interface CommandLoggerOptions { + verbose?: boolean; + constructorName: string; +} + +export class CommandLogger { + private logger: Logger; + private verbose: boolean; + + constructor(options: CommandLoggerOptions) { + this.logger = new Logger(options.constructorName); + this.verbose = options.verbose ?? true; + } + + log(message: string, context?: string) { + if (this.verbose) { + this.logger.log(message, context); + } + } + + error(message: string, stack?: string, context?: string) { + if (this.verbose) { + this.logger.error(message, stack, context); + } + } + + warn(message: string, context?: string) { + if (this.verbose) { + this.logger.warn(message, context); + } + } + + debug(message: string, context?: string) { + if (this.verbose) { + this.logger.debug(message, context); + } + } + + verboseLog(message: string, context?: string) { + if (this.verbose) { + this.logger.verbose(message, context); + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-phone-calling-code-create-column.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-phone-calling-code-create-column.command.ts new file mode 100644 index 000000000000..ea3750ab32e8 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-phone-calling-code-create-column.command.ts @@ -0,0 +1,144 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import { Command } from 'nest-commander'; +import { Repository } from 'typeorm'; +import { v4 } from 'uuid'; + +import { + ActiveWorkspacesCommandOptions, + ActiveWorkspacesCommandRunner, +} from 'src/database/commands/active-workspaces.command'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { + FieldMetadataEntity, + FieldMetadataType, +} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; +import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; +import { + WorkspaceMigrationColumnActionType, + WorkspaceMigrationTableAction, + WorkspaceMigrationTableActionType, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; +import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; +import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; +import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; +import { isDefined } from 'src/utils/is-defined'; + +@Command({ + name: 'upgrade-0.40:phone-calling-code-create-column', + description: 'Create the callingCode column', +}) +export class PhoneCallingCodeCreateColumnCommand extends ActiveWorkspacesCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + private readonly workspaceMigrationService: WorkspaceMigrationService, + private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, + private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, + private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, + ) { + super(workspaceRepository); + } + + async executeActiveWorkspacesCommand( + _passedParam: string[], + options: ActiveWorkspacesCommandOptions, + workspaceIds: string[], + ): Promise { + this.logger.log( + 'Running command to add calling code and change country code with default one', + ); + + this.logger.log(`Part 1 - Workspace`); + let workspaceIterator = 1; + + for (const workspaceId of workspaceIds) { + this.logger.log( + `Running command for workspace ${workspaceId} ${workspaceIterator}/${workspaceIds.length}`, + ); + + this.logger.log( + `P1 Step 1 - let's find all the fieldsMetadata that have the PHONES type, and extract the objectMetadataId`, + ); + + try { + const phonesFieldMetadata = await this.fieldMetadataRepository.find({ + where: { + workspaceId, + type: FieldMetadataType.PHONES, + }, + relations: ['object'], + }); + + for (const phoneFieldMetadata of phonesFieldMetadata) { + if ( + isDefined(phoneFieldMetadata?.name && phoneFieldMetadata.object) + ) { + this.logger.log( + `P1 Step 1 - Let's find the "nameSingular" of this objectMetadata: ${phoneFieldMetadata.object.nameSingular || 'not found'}`, + ); + + if (!phoneFieldMetadata.object?.nameSingular) continue; + + this.logger.log( + `P1 Step 1 - Create migration for field ${phoneFieldMetadata.name}`, + ); + + if (options.dryRun === true) { + continue; + } + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName( + `create-${phoneFieldMetadata.object.nameSingular}PrimaryPhoneCallingCode-for-field-${phoneFieldMetadata.name}`, + ), + workspaceId, + [ + { + name: computeObjectTargetTable(phoneFieldMetadata.object), + action: WorkspaceMigrationTableActionType.ALTER, + columns: this.workspaceMigrationFactory.createColumnActions( + WorkspaceMigrationColumnActionType.CREATE, + { + id: v4(), + type: FieldMetadataType.TEXT, + name: `${phoneFieldMetadata.name}PrimaryPhoneCallingCode`, + label: `${phoneFieldMetadata.name}PrimaryPhoneCallingCode`, + objectMetadataId: phoneFieldMetadata.object.id, + workspaceId: workspaceId, + isNullable: true, + defaultValue: "''", + } satisfies Partial, + ), + } satisfies WorkspaceMigrationTableAction, + ], + ); + } + } + + this.logger.log( + `P1 Step 1 - RUN migration to create callingCodes for ${workspaceId.slice(0, 5)}`, + ); + await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( + workspaceId, + ); + + await this.workspaceMetadataVersionService.incrementMetadataVersion( + workspaceId, + ); + } catch (error) { + console.log(`Error in workspace ${workspaceId} : ${error}`); + } + workspaceIterator++; + } + + this.logger.log(chalk.green(`Command completed!`)); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-phone-calling-code-migrate-data.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-phone-calling-code-migrate-data.command.ts new file mode 100644 index 000000000000..ac60af59bc90 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-phone-calling-code-migrate-data.command.ts @@ -0,0 +1,302 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import { getCountries, getCountryCallingCode } from 'libphonenumber-js'; +import { Command } from 'nest-commander'; +import { Repository } from 'typeorm'; +import { v4 } from 'uuid'; + +import { + ActiveWorkspacesCommandOptions, + ActiveWorkspacesCommandRunner, +} from 'src/database/commands/active-workspaces.command'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { + FieldMetadataEntity, + FieldMetadataType, +} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; +import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; +import { + WorkspaceMigrationColumnActionType, + WorkspaceMigrationTableAction, + WorkspaceMigrationTableActionType, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; +import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; +import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; +import { isDefined } from 'src/utils/is-defined'; + +const callingCodeToCountryCode = (callingCode: string): string => { + if (!callingCode) { + return ''; + } + let callingCodeSanitized = callingCode; + + if (callingCode.startsWith('+')) { + callingCodeSanitized = callingCode.slice(1); + } + + return ( + getCountries().find( + (countryCode) => + getCountryCallingCode(countryCode) === callingCodeSanitized, + ) || '' + ); +}; + +const isCallingCode = (callingCode: string): boolean => { + return callingCodeToCountryCode(callingCode) !== ''; +}; + +@Command({ + name: 'upgrade-0.40:phone-calling-code-migrate-data', + description: 'Add calling code and change country code with default one', +}) +export class PhoneCallingCodeMigrateDataCommand extends ActiveWorkspacesCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly workspaceMigrationService: WorkspaceMigrationService, + private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, + private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, + private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, + ) { + super(workspaceRepository); + } + + async executeActiveWorkspacesCommand( + _passedParam: string[], + options: ActiveWorkspacesCommandOptions, + workspaceIds: string[], + ): Promise { + this.logger.log( + 'Running command to add calling code and change country code with default one', + ); + + this.logger.log(`Part 1 - Workspace`); + + let workspaceIterator = 1; + + for (const workspaceId of workspaceIds) { + this.logger.log( + `Running command for workspace ${workspaceId} ${workspaceIterator}/${workspaceIds.length}`, + ); + + this.logger.log( + `P1 Step 1 - let's find all the fieldsMetadata that have the PHONES type, and extract the objectMetadataId`, + ); + + try { + const phonesFieldMetadata = await this.fieldMetadataRepository.find({ + where: { + workspaceId, + type: FieldMetadataType.PHONES, + }, + relations: ['object'], + }); + + for (const phoneFieldMetadata of phonesFieldMetadata) { + if ( + isDefined(phoneFieldMetadata?.name) && + isDefined(phoneFieldMetadata.object) + ) { + this.logger.log( + `P1 Step 1 - Let's find the "nameSingular" of this objectMetadata: ${phoneFieldMetadata.object.nameSingular || 'not found'}`, + ); + + if (!phoneFieldMetadata.object?.nameSingular) continue; + + this.logger.log( + `P1 Step 1 - Create migration for field ${phoneFieldMetadata.name}`, + ); + if (options.dryRun === false) { + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName( + `create-${phoneFieldMetadata.object.nameSingular}PrimaryPhoneCallingCode-for-field-${phoneFieldMetadata.name}`, + ), + workspaceId, + [ + { + name: computeObjectTargetTable(phoneFieldMetadata.object), + action: WorkspaceMigrationTableActionType.ALTER, + columns: this.workspaceMigrationFactory.createColumnActions( + WorkspaceMigrationColumnActionType.CREATE, + { + id: v4(), + type: FieldMetadataType.TEXT, + name: `${phoneFieldMetadata.name}PrimaryPhoneCallingCode`, + label: `${phoneFieldMetadata.name}PrimaryPhoneCallingCode`, + objectMetadataId: phoneFieldMetadata.object.id, + workspaceId: workspaceId, + isNullable: true, + defaultValue: "''", + }, + ), + } satisfies WorkspaceMigrationTableAction, + ], + ); + } + } + } + + this.logger.log( + `P1 Step 1 - RUN migration to create callingCodes for ${workspaceId.slice(0, 5)}`, + ); + await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( + workspaceId, + ); + + await this.workspaceMetadataVersionService.incrementMetadataVersion( + workspaceId, + ); + + this.logger.log( + `P1 Step 2 - Migrations for callingCode must be first. Now can use twentyORMGlobalManager to update countryCode`, + ); + + this.logger.log( + `P1 Step 3 (same time) - update CountryCode to letters: +33 => FR || +1 => US (if mulitple, first one)`, + ); + + this.logger.log( + `P1 Step 4 (same time) - update all additioanl phones to add a country code following the same logic`, + ); + + for (const phoneFieldMetadata of phonesFieldMetadata) { + this.logger.log(`P1 Step 2 - for ${phoneFieldMetadata.name}`); + if ( + isDefined(phoneFieldMetadata) && + isDefined(phoneFieldMetadata.name) + ) { + const [objectMetadata] = await this.objectMetadataRepository.find({ + where: { + id: phoneFieldMetadata?.objectMetadataId, + }, + }); + + const repository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + objectMetadata.nameSingular, + ); + const records = await repository.find(); + + for (const record of records) { + if ( + record?.[phoneFieldMetadata.name]?.primaryPhoneCountryCode && + isCallingCode( + record[phoneFieldMetadata.name].primaryPhoneCountryCode, + ) + ) { + let additionalPhones = null; + + if (record[phoneFieldMetadata.name].additionalPhones) { + additionalPhones = record[ + phoneFieldMetadata.name + ].additionalPhones.map((phone) => { + return { + ...phone, + countryCode: callingCodeToCountryCode(phone.callingCode), + }; + }); + } + if (options.dryRun === false) { + await repository.update(record.id, { + [`${phoneFieldMetadata.name}PrimaryPhoneCallingCode`]: + record[phoneFieldMetadata.name].primaryPhoneCountryCode, + [`${phoneFieldMetadata.name}PrimaryPhoneCountryCode`]: + callingCodeToCountryCode( + record[phoneFieldMetadata.name].primaryPhoneCountryCode, + ), + [`${phoneFieldMetadata.name}AdditionalPhones`]: + additionalPhones, + }); + } + } + } + } + } + } catch (error) { + console.log(`Error in workspace ${workspaceId} : ${error}`); + } + workspaceIterator++; + } + + this.logger.log(` + + Part 2 - FieldMetadata`); + + workspaceIterator = 1; + for (const workspaceId of workspaceIds) { + this.logger.log( + `Running command for workspace ${workspaceId} ${workspaceIterator}/${workspaceIds.length}`, + ); + + this.logger.log( + `P2 Step 1 - let's find all the fieldsMetadata that have the PHONES type, and extract the objectMetadataId`, + ); + + try { + const phonesFieldMetadata = await this.fieldMetadataRepository.find({ + where: { + workspaceId, + type: FieldMetadataType.PHONES, + }, + }); + + for (const phoneFieldMetadata of phonesFieldMetadata) { + if ( + !isDefined(phoneFieldMetadata) || + !isDefined(phoneFieldMetadata.defaultValue) + ) + continue; + let defaultValue = phoneFieldMetadata.defaultValue; + + // some cases look like it's an array. let's flatten it (not sure the case is supposed to happen but I saw it in my local db) + if (Array.isArray(defaultValue) && isDefined(defaultValue[0])) + defaultValue = phoneFieldMetadata.defaultValue[0]; + + if (!isDefined(defaultValue)) continue; + if (typeof defaultValue !== 'object') continue; + if (!('primaryPhoneCountryCode' in defaultValue)) continue; + if (!defaultValue.primaryPhoneCountryCode) continue; + + const primaryPhoneCountryCode = defaultValue.primaryPhoneCountryCode; + + const countryCode = callingCodeToCountryCode( + primaryPhoneCountryCode.replace(/["']/g, ''), + ); + + if (options.dryRun === false) { + await this.fieldMetadataRepository.update(phoneFieldMetadata.id, { + defaultValue: { + ...defaultValue, + primaryPhoneCountryCode: countryCode + ? `'${countryCode}'` + : "''", + primaryPhoneCallingCode: isCallingCode( + primaryPhoneCountryCode.replace(/["']/g, ''), + ) + ? primaryPhoneCountryCode + : "''", + }, + }); + } + } + } catch (error) { + console.log(`Error in workspace ${workspaceId} : ${error}`); + } + workspaceIterator++; + } + this.logger.log(chalk.green(`Command completed!`)); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-upgrade-version.command.ts index 767c1486ec6f..f0063834a52d 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-upgrade-version.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-upgrade-version.command.ts @@ -5,6 +5,8 @@ import { Repository } from 'typeorm'; import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command'; import { BaseCommandOptions } from 'src/database/commands/base.command'; +import { PhoneCallingCodeCreateColumnCommand } from 'src/database/commands/upgrade-version/0-40/0-40-phone-calling-code-create-column.command'; +import { PhoneCallingCodeMigrateDataCommand } from 'src/database/commands/upgrade-version/0-40/0-40-phone-calling-code-migrate-data.command'; import { RecordPositionBackfillCommand } from 'src/database/commands/upgrade-version/0-40/0-40-record-position-backfill.command'; import { ViewGroupNoValueBackfillCommand } from 'src/database/commands/upgrade-version/0-40/0-40-view-group-no-value-backfill.command'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -18,9 +20,11 @@ export class UpgradeTo0_40Command extends ActiveWorkspacesCommandRunner { constructor( @InjectRepository(Workspace, 'core') protected readonly workspaceRepository: Repository, - private readonly recordPositionBackfillCommand: RecordPositionBackfillCommand, private readonly viewGroupNoValueBackfillCommand: ViewGroupNoValueBackfillCommand, private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand, + private readonly phoneCallingCodeMigrateDataCommand: PhoneCallingCodeMigrateDataCommand, + private readonly phoneCallingCodeCreateColumnCommand: PhoneCallingCodeCreateColumnCommand, + private readonly recordPositionBackfillCommand: RecordPositionBackfillCommand, ) { super(workspaceRepository); } @@ -30,6 +34,22 @@ export class UpgradeTo0_40Command extends ActiveWorkspacesCommandRunner { options: BaseCommandOptions, workspaceIds: string[], ): Promise { + this.logger.log( + 'Running command to upgrade to 0.40: must start with phone calling code otherwise SyncMetadata will fail', + ); + + await this.phoneCallingCodeCreateColumnCommand.executeActiveWorkspacesCommand( + passedParam, + options, + workspaceIds, + ); + + await this.phoneCallingCodeMigrateDataCommand.executeActiveWorkspacesCommand( + passedParam, + options, + workspaceIds, + ); + await this.recordPositionBackfillCommand.executeActiveWorkspacesCommand( passedParam, options, diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-upgrade-version.module.ts index ccd92107f68b..b19780ed8a17 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-upgrade-version.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-40/0-40-upgrade-version.module.ts @@ -1,6 +1,8 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { PhoneCallingCodeCreateColumnCommand } from 'src/database/commands/upgrade-version/0-40/0-40-phone-calling-code-create-column.command'; +import { PhoneCallingCodeMigrateDataCommand } from 'src/database/commands/upgrade-version/0-40/0-40-phone-calling-code-migrate-data.command'; import { RecordPositionBackfillCommand } from 'src/database/commands/upgrade-version/0-40/0-40-record-position-backfill.command'; import { UpgradeTo0_40Command } from 'src/database/commands/upgrade-version/0-40/0-40-upgrade-version.command'; import { ViewGroupNoValueBackfillCommand } from 'src/database/commands/upgrade-version/0-40/0-40-view-group-no-value-backfill.command'; @@ -8,18 +10,35 @@ import { RecordPositionBackfillModule } from 'src/engine/api/graphql/workspace-q import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { SearchModule } from 'src/engine/metadata-modules/search/search.module'; +import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; +import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; +import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; +import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module'; @Module({ imports: [ TypeOrmModule.forFeature([Workspace], 'core'), + TypeOrmModule.forFeature( + [ObjectMetadataEntity, FieldMetadataEntity], + 'metadata', + ), TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'), WorkspaceSyncMetadataCommandsModule, + SearchModule, + WorkspaceMigrationRunnerModule, + WorkspaceMetadataVersionModule, + WorkspaceMigrationModule, RecordPositionBackfillModule, FieldMetadataModule, ], providers: [ UpgradeTo0_40Command, + PhoneCallingCodeMigrateDataCommand, + PhoneCallingCodeCreateColumnCommand, + WorkspaceMigrationFactory, RecordPositionBackfillCommand, ViewGroupNoValueBackfillCommand, ], diff --git a/packages/twenty-server/src/database/typeorm-seeds/metadata/fieldsMetadata.ts b/packages/twenty-server/src/database/typeorm-seeds/metadata/fieldsMetadata.ts index a0b8081c9d59..fd0bfe2c1279 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/metadata/fieldsMetadata.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/metadata/fieldsMetadata.ts @@ -106,13 +106,12 @@ export const getDevSeedPeopleCustomFields = ( isActive: true, isNullable: false, isUnique: false, - defaultValue: [ - { - primaryPhoneNumber: '', - primaryPhoneCountryCode: '', - additionalPhones: {}, - }, - ], + defaultValue: { + primaryPhoneNumber: "''", + primaryPhoneCountryCode: "'FR'", + primaryPhoneCallingCode: "'+33'", + additionalPhones: null, + }, objectMetadataId, }, { diff --git a/packages/twenty-server/src/database/typeorm-seeds/workspace/people.ts b/packages/twenty-server/src/database/typeorm-seeds/workspace/people.ts index 22adfe0142e9..8fead7f84fae 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/workspace/people.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/workspace/people.ts @@ -35,12 +35,14 @@ export const seedPeople = async ( 'nameFirstName', 'nameLastName', 'phonesPrimaryPhoneCountryCode', + 'phonesPrimaryPhoneCallingCode', 'phonesPrimaryPhoneNumber', 'city', 'companyId', 'emailsPrimaryEmail', 'position', 'whatsappPrimaryPhoneCountryCode', + 'whatsappPrimaryPhoneCallingCode', 'whatsappPrimaryPhoneNumber', 'createdBySource', 'createdByWorkspaceMemberId', @@ -52,13 +54,15 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.CHRISTOPH, nameFirstName: 'Christoph', nameLastName: 'Callisto', - phonePrimaryPhoneCountryCode: '+33', - phonePrimaryPhoneNumber: '789012345', + phonesPrimaryPhoneCountryCode: 'FR', + phonesPrimaryPhoneCallingCode: '+33', + phonesPrimaryPhoneNumber: '789012345', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.LINKEDIN, emailsPrimaryEmail: 'christoph.calisto@linkedin.com', position: 1, - whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneCountryCode: 'FR', + whatsappPrimaryPhoneCallingCode: '+33', whatsappPrimaryPhoneNumber: '789012345', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, @@ -68,13 +72,15 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.SYLVIE, nameFirstName: 'Sylvie', nameLastName: 'Palmer', - phonePrimaryPhoneCountryCode: '+33', - phonePrimaryPhoneNumber: '780123456', + phonesPrimaryPhoneCountryCode: 'FR', + phonesPrimaryPhoneCallingCode: '+33', + phonesPrimaryPhoneNumber: '780123456', city: 'Los Angeles', companyId: DEV_SEED_COMPANY_IDS.LINKEDIN, emailsPrimaryEmail: 'sylvie.palmer@linkedin.com', position: 2, - whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneCountryCode: 'FR', + whatsappPrimaryPhoneCallingCode: '+33', whatsappPrimaryPhoneNumber: '780123456', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, @@ -84,13 +90,15 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.CHRISTOPHER_G, nameFirstName: 'Christopher', nameLastName: 'Gonzalez', - phonePrimaryPhoneCountryCode: '+33', - phonePrimaryPhoneNumber: '789012345', + phonesPrimaryPhoneCountryCode: 'FR', + phonesPrimaryPhoneCallingCode: '+33', + phonesPrimaryPhoneNumber: '789012345', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.QONTO, emailsPrimaryEmail: 'christopher.gonzalez@qonto.com', position: 3, - whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneCountryCode: 'FR', + whatsappPrimaryPhoneCallingCode: '+33', whatsappPrimaryPhoneNumber: '789012345', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, @@ -100,13 +108,15 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.ASHLEY, nameFirstName: 'Ashley', nameLastName: 'Parker', - phonePrimaryPhoneCountryCode: '+33', - phonePrimaryPhoneNumber: '780123456', + phonesPrimaryPhoneCountryCode: 'FR', + phonesPrimaryPhoneCallingCode: '+33', + phonesPrimaryPhoneNumber: '780123456', city: 'Los Angeles', companyId: DEV_SEED_COMPANY_IDS.QONTO, emailsPrimaryEmail: 'ashley.parker@qonto.com', position: 4, - whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneCountryCode: 'FR', + whatsappPrimaryPhoneCallingCode: '+33', whatsappPrimaryPhoneNumber: '780123456', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, @@ -116,13 +126,15 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.NICHOLAS, nameFirstName: 'Nicholas', nameLastName: 'Wright', - phonePrimaryPhoneCountryCode: '+33', - phonePrimaryPhoneNumber: '781234567', + phonesPrimaryPhoneCountryCode: 'FR', + phonesPrimaryPhoneCallingCode: '+33', + phonesPrimaryPhoneNumber: '781234567', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.MICROSOFT, emailsPrimaryEmail: 'nicholas.wright@microsoft.com', position: 5, - whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneCountryCode: 'FR', + whatsappPrimaryPhoneCallingCode: '+33', whatsappPrimaryPhoneNumber: '781234567', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, @@ -132,13 +144,15 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.ISABELLA, nameFirstName: 'Isabella', nameLastName: 'Scott', - phonePrimaryPhoneCountryCode: '+33', - phonePrimaryPhoneNumber: '782345678', + phonesPrimaryPhoneCountryCode: 'FR', + phonesPrimaryPhoneCallingCode: '+33', + phonesPrimaryPhoneNumber: '782345678', city: 'New York', companyId: DEV_SEED_COMPANY_IDS.MICROSOFT, emailsPrimaryEmail: 'isabella.scott@microsoft.com', position: 6, - whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneCountryCode: 'FR', + whatsappPrimaryPhoneCallingCode: '+33', whatsappPrimaryPhoneNumber: '782345678', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, @@ -148,13 +162,15 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.MATTHEW, nameFirstName: 'Matthew', nameLastName: 'Green', - phonePrimaryPhoneCountryCode: '+33', - phonePrimaryPhoneNumber: '783456789', + phonesPrimaryPhoneCountryCode: 'FR', + phonesPrimaryPhoneCallingCode: '+33', + phonesPrimaryPhoneNumber: '783456789', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.MICROSOFT, emailsPrimaryEmail: 'matthew.green@microsoft.com', position: 7, - whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneCountryCode: 'FR', + whatsappPrimaryPhoneCallingCode: '+33', whatsappPrimaryPhoneNumber: '783456789', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, @@ -164,13 +180,15 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.ELIZABETH, nameFirstName: 'Elizabeth', nameLastName: 'Baker', - phonePrimaryPhoneCountryCode: '+33', - phonePrimaryPhoneNumber: '784567890', + phonesPrimaryPhoneCountryCode: 'FR', + phonesPrimaryPhoneCallingCode: '+33', + phonesPrimaryPhoneNumber: '784567890', city: 'New York', companyId: DEV_SEED_COMPANY_IDS.AIRBNB, emailsPrimaryEmail: 'elizabeth.baker@airbnb.com', position: 8, - whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneCountryCode: 'FR', + whatsappPrimaryPhoneCallingCode: '+33', whatsappPrimaryPhoneNumber: '784567890', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, @@ -180,13 +198,15 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.CHRISTOPHER_N, nameFirstName: 'Christopher', nameLastName: 'Nelson', - phonePrimaryPhoneCountryCode: '+33', - phonePrimaryPhoneNumber: '785678901', + phonesPrimaryPhoneCountryCode: 'FR', + phonesPrimaryPhoneCallingCode: '+33', + phonesPrimaryPhoneNumber: '785678901', city: 'San Francisco', companyId: DEV_SEED_COMPANY_IDS.AIRBNB, emailsPrimaryEmail: 'christopher.nelson@airbnb.com', position: 9, - whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneCountryCode: 'FR', + whatsappPrimaryPhoneCallingCode: '+33', whatsappPrimaryPhoneNumber: '785678901', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, @@ -196,13 +216,15 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.AVERY, nameFirstName: 'Avery', nameLastName: 'Carter', - phonePrimaryPhoneCountryCode: '+33', - phonePrimaryPhoneNumber: '786789012', + phonesPrimaryPhoneCountryCode: 'FR', + phonesPrimaryPhoneCallingCode: '+33', + phonesPrimaryPhoneNumber: '786789012', city: 'New York', companyId: DEV_SEED_COMPANY_IDS.AIRBNB, emailsPrimaryEmail: 'avery.carter@airbnb.com', position: 10, - whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneCountryCode: 'FR', + whatsappPrimaryPhoneCallingCode: '+33', whatsappPrimaryPhoneNumber: '786789012', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, @@ -212,13 +234,15 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.ETHAN, nameFirstName: 'Ethan', nameLastName: 'Mitchell', - phonePrimaryPhoneCountryCode: '+33', - phonePrimaryPhoneNumber: '787890123', + phonesPrimaryPhoneCountryCode: 'FR', + phonesPrimaryPhoneCallingCode: '+33', + phonesPrimaryPhoneNumber: '787890123', city: 'Los Angeles', companyId: DEV_SEED_COMPANY_IDS.GOOGLE, emailsPrimaryEmail: 'ethan.mitchell@google.com', position: 11, - whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneCountryCode: 'FR', + whatsappPrimaryPhoneCallingCode: '+33', whatsappPrimaryPhoneNumber: '787890123', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, @@ -228,13 +252,15 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.MADISON, nameFirstName: 'Madison', nameLastName: 'Perez', - phonePrimaryPhoneCountryCode: '+33', - phonePrimaryPhoneNumber: '788901234', + phonesPrimaryPhoneCountryCode: 'FR', + phonesPrimaryPhoneCallingCode: '+33', + phonesPrimaryPhoneNumber: '788901234', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.GOOGLE, emailsPrimaryEmail: 'madison.perez@google.com', position: 12, - whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneCountryCode: 'FR', + whatsappPrimaryPhoneCallingCode: '+33', whatsappPrimaryPhoneNumber: '788901234', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, @@ -244,13 +270,15 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.BERTRAND, nameFirstName: 'Bertrand', nameLastName: 'Voulzy', - phonePrimaryPhoneCountryCode: '+33', - phonePrimaryPhoneNumber: '788901234', + phonesPrimaryPhoneCountryCode: 'FR', + phonesPrimaryPhoneCallingCode: '+33', + phonesPrimaryPhoneNumber: '788901234', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.GOOGLE, emailsPrimaryEmail: 'bertrand.voulzy@google.com', position: 13, - whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneCountryCode: 'FR', + whatsappPrimaryPhoneCallingCode: '+33', whatsappPrimaryPhoneNumber: '788901234', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, @@ -260,13 +288,15 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.LOUIS, nameFirstName: 'Louis', nameLastName: 'Duss', - phonePrimaryPhoneCountryCode: '+33', - phonePrimaryPhoneNumber: '789012345', + phonesPrimaryPhoneCountryCode: 'FR', + phonesPrimaryPhoneCallingCode: '+33', + phonesPrimaryPhoneNumber: '789012345', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.GOOGLE, emailsPrimaryEmail: 'louis.duss@google.com', position: 14, - whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneCountryCode: 'FR', + whatsappPrimaryPhoneCallingCode: '+33', whatsappPrimaryPhoneNumber: '789012345', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, @@ -276,13 +306,15 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.LORIE, nameFirstName: 'Lorie', nameLastName: 'Vladim', - phonePrimaryPhoneCountryCode: '+33', - phonePrimaryPhoneNumber: '788901235', + phonesPrimaryPhoneCountryCode: 'FR', + phonesPrimaryPhoneCallingCode: '+33', + phonesPrimaryPhoneNumber: '788901235', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.GOOGLE, emailsPrimaryEmail: 'lorie.vladim@google.com', position: 15, - whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneCountryCode: 'FR', + whatsappPrimaryPhoneCallingCode: '+33', whatsappPrimaryPhoneNumber: '788901235', createdBySource: 'MANUAL', createdByWorkspaceMemberId: null, diff --git a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts index 77f1a24e447e..093b4a0efbee 100644 --- a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts +++ b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts @@ -231,6 +231,7 @@ const fieldPhonesMock = { { primaryPhoneNumber: '', primaryPhoneCountryCode: '', + primaryPhoneCallingCode: '', additionalPhones: {}, }, ], diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts index a5ce9aa30023..fb229a1f8eb5 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts @@ -150,6 +150,7 @@ export const mapFieldMetadataToGraphqlQuery = ( { primaryPhoneNumber primaryPhoneCountryCode + primaryPhoneCallingCode additionalPhones } `; diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts index c414c6fe3151..75ac244b8636 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts @@ -31,6 +31,9 @@ describe('computeSchemaComponents', () => { primaryPhoneCountryCode: { type: 'string', }, + primaryPhoneCallingCode: { + type: 'string', + }, primaryPhoneNumber: { type: 'string', }, @@ -216,6 +219,9 @@ describe('computeSchemaComponents', () => { primaryPhoneCountryCode: { type: 'string', }, + primaryPhoneCallingCode: { + type: 'string', + }, primaryPhoneNumber: { type: 'string', }, @@ -400,6 +406,9 @@ describe('computeSchemaComponents', () => { primaryPhoneCountryCode: { type: 'string', }, + primaryPhoneCallingCode: { + type: 'string', + }, primaryPhoneNumber: { type: 'string', }, diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts index 8b5d33589640..4d93e29c070e 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts @@ -259,6 +259,9 @@ const getSchemaComponentsProperties = ({ primaryPhoneCountryCode: { type: 'string', }, + primaryPhoneCallingCode: { + type: 'string', + }, primaryPhoneNumber: { type: 'string', }, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type.ts index 366e957545cb..6f635f63bf21 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type.ts @@ -18,6 +18,12 @@ export const phonesCompositeType: CompositeType = { hidden: false, isRequired: false, }, + { + name: 'primaryPhoneCallingCode', + type: FieldMetadataType.TEXT, + hidden: false, + isRequired: false, + }, { name: 'additionalPhones', type: FieldMetadataType.RAW_JSON, @@ -30,5 +36,6 @@ export const phonesCompositeType: CompositeType = { export type PhonesMetadata = { primaryPhoneNumber: string; primaryPhoneCountryCode: string; + primaryPhoneCallingCode: string; additionalPhones: object | null; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts index 99bf6e07fe83..68b88bc28fd0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts @@ -193,6 +193,10 @@ export class FieldMetadataDefaultValuePhones { @IsQuotedString() primaryPhoneCountryCode: string | null; + @ValidateIf((_object, value) => value !== null) + @IsQuotedString() + primaryPhoneCallingCode: string | null; + @ValidateIf((_object, value) => value !== null) @IsObject() additionalPhones: object | null; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts index ac3642814c8d..17b55a4b6622 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts @@ -44,6 +44,7 @@ export function generateDefaultValue( return { primaryPhoneNumber: "''", primaryPhoneCountryCode: "''", + primaryPhoneCallingCode: "''", additionalPhones: null, }; default: diff --git a/packages/twenty-zapier/src/test/creates/crud_record.test.ts b/packages/twenty-zapier/src/test/creates/crud_record.test.ts index 5065c8778176..ff4dfc46ddf1 100644 --- a/packages/twenty-zapier/src/test/creates/crud_record.test.ts +++ b/packages/twenty-zapier/src/test/creates/crud_record.test.ts @@ -60,8 +60,11 @@ describe('creates.create_company', () => { name: { firstName: 'John', lastName: 'Doe' }, phones: { primaryPhoneNumber: '610203040', - primaryPhoneCountryCode: '+33', - additionalPhones: ['{number: "610203041", countryCode: "+33"}'], + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', + additionalPhones: [ + '{number: "610203041", countryCode: "FR", callingCode: "+33"}', + ], }, city: 'Paris', }); diff --git a/packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts b/packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts index d01fb2b38b76..6453b906ff44 100644 --- a/packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts +++ b/packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts @@ -25,8 +25,11 @@ describe('utils.handleQueryParams', () => { }, phones: { primaryPhoneNumber: '322110011', - primaryPhoneCountryCode: '+33', - additionalPhones: ["{ phoneNumber: '322110012', countryCode: '+33' }"], + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '+33', + additionalPhones: [ + "{ phoneNumber: '322110012', countryCode: 'FR', callingCode: '+33' }", + ], }, xUrl__url: '/x_url', xUrl__label: 'Test xUrl', @@ -42,7 +45,7 @@ describe('utils.handleQueryParams', () => { 'linkedinUrl: {url: "/linkedin_url", label: "Test linkedinUrl"}, ' + 'whatsapp: {primaryLinkUrl: "/whatsapp_url", primaryLinkLabel: "Whatsapp Link", secondaryLinks: [{url: \'/secondary_whatsapp_url\',label: \'Secondary Whatsapp Link\'}]}, ' + 'emails: {primaryEmail: "primary@email.com", additionalEmails: ["secondary@email.com"]}, ' + - 'phones: {primaryPhoneNumber: "322110011", primaryPhoneCountryCode: "+33", additionalPhones: [{ phoneNumber: \'322110012\', countryCode: \'+33\' }]}, ' + + 'phones: {primaryPhoneNumber: "322110011", primaryPhoneCountryCode: "FR", primaryPhoneCallingCode: "+33", additionalPhones: [{ phoneNumber: \'322110012\', countryCode: \'+33\' }]}, ' + 'xUrl: {url: "/x_url", label: "Test xUrl"}, ' + 'annualRecurringRevenue: 100000, ' + 'idealCustomerProfile: true, ' +