diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.tsx b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.tsx index da578f897..215cb8af8 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.tsx @@ -197,6 +197,10 @@ name lastName } phone +{ + primaryPhoneNumber + primaryPhoneCountryCode +} linkedinLink { primaryLinkUrl diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.tsx b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.tsx index 5a0090964..4ab9bebb9 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.tsx @@ -46,7 +46,11 @@ describe('mapObjectMetadataToGraphQLQuery', () => { primaryEmail additionalEmails } - phone + phone + { + primaryPhoneNumber + primaryPhoneCountryCode + } createdAt avatarUrl jobTitle diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts b/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts index 2b2fd70bb..7fd6050ad 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts @@ -107,7 +107,7 @@ export const getObjectMetadataItemsMock = () => { { "__typename": "field", "id": "194ff398-99f9-4cbb-b87a-e44408f9c1ed", - "type": "PHONE", + "type": "PHONES", "name": "whatsapp", "label": "Whatsapp", "description": "Contact's Whatsapp Number", @@ -614,7 +614,7 @@ export const getObjectMetadataItemsMock = () => { { "__typename": "field", "id": "9c2bf923-304d-47b7-beb0-286e3229f6ac", - "type": "TEXT", + "type": "PHONES", "name": "phone", "label": "Phone", "description": "Contact’s phone number", diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/personFragment.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/personFragment.ts index 2f0fdfcee..6e109248b 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/personFragment.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/personFragment.ts @@ -2,7 +2,11 @@ export const PERSON_FRAGMENT = ` __typename updatedAt myCustomObjectId - whatsapp + whatsapp { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } linkedinLink { primaryLinkUrl primaryLinkLabel @@ -28,7 +32,11 @@ export const PERSON_FRAGMENT = ` } performanceRating createdAt - phone + phone { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } id city companyId 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 967f1ac6a..fd2830716 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 @@ -36,7 +36,10 @@ export const responseData = { firstName: '', lastName: '', }, - phone: '', + phones: { + primaryPhoneCountryCode: '', + primaryPhoneNumber: '', + }, linkedinLink: { primaryLinkUrl: '', primaryLinkLabel: '', 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 cb22a045d..6804a92c3 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 @@ -41,7 +41,10 @@ export const responseData = { firstName: '', lastName: '', }, - phone: '', + phones: { + primaryPhoneCountryCode: '', + primaryPhoneNumber: '', + }, linkedinLink: { primaryLinkUrl: '', primaryLinkLabel: '', diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useUpdateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useUpdateOneRecord.ts index 4cc34f493..44fe83664 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useUpdateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useUpdateOneRecord.ts @@ -24,7 +24,6 @@ const basePerson = { firstName: '', lastName: '', }, - phone: '', linkedinLink: { primaryLinkUrl: '', primaryLinkLabel: '', diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecord.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecord.test.tsx index 0ccd92897..eb6e7048d 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecord.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecord.test.tsx @@ -1,6 +1,6 @@ -import { ReactNode } from 'react'; import { MockedProvider } from '@apollo/client/testing'; import { act, renderHook } from '@testing-library/react'; +import { ReactNode } from 'react'; import { RecoilRoot } from 'recoil'; import { @@ -11,8 +11,17 @@ import { import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; const person = { id: '36abbb63-34ed-4a16-89f5-f549ac55d0f9' }; -const update = { name: { firstName: 'John', lastName: 'Doe' } }; -const updatePerson = { ...person, ...responseData, ...update }; +const update = { + name: { + firstName: 'John', + lastName: 'Doe', + }, +}; +const updatePerson = { + ...person, + ...responseData, + ...update, +}; const mocks = [ { diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromString.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromString.ts index d41d5fd85..c668801e3 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromString.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromString.ts @@ -6,10 +6,12 @@ import { isFieldAddress } from '@/object-record/record-field/types/guards/isFiel import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency'; import { isFieldDateTime } from '@/object-record/record-field/types/guards/isFieldDateTime'; import { isFieldEmail } from '@/object-record/record-field/types/guards/isFieldEmail'; +import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails'; import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName'; import { isFieldLink } from '@/object-record/record-field/types/guards/isFieldLink'; import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks'; import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber'; +import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones'; import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation'; import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText'; import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid'; @@ -66,5 +68,17 @@ export const computeDraftValueFromString = ({ } as FieldInputDraftValue; } + if (isFieldEmails(fieldDefinition)) { + return { + primaryEmail: value, + } as FieldInputDraftValue; + } + + if (isFieldPhones(fieldDefinition)) { + return { + primaryPhoneNumber: value, + } as FieldInputDraftValue; + } + throw new Error(`Record field type not supported : ${fieldDefinition.type}}`); }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useTableData.test.tsx b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useTableData.test.tsx index bac46e652..2cacb3e8c 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useTableData.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useTableData.test.tsx @@ -1,4 +1,5 @@ import { act, renderHook, waitFor } from '@testing-library/react'; +import { ReactNode } from 'react'; import { percentage, sleep, useTableData } from '../useTableData'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; @@ -9,7 +10,6 @@ import { extractComponentState } from '@/ui/utilities/state/component-state/util import { ViewType } from '@/views/types/ViewType'; import { MockedProvider, MockedResponse } from '@apollo/client/testing'; import gql from 'graphql-tag'; -import { ReactNode } from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; import { RecoilRoot, useRecoilValue } from 'recoil'; @@ -26,7 +26,11 @@ const mockPerson = { __typename: 'Person', updatedAt: '2021-08-03T19:20:06.000Z', myCustomObjectId: '123', - whatsapp: '123', + whatsapp: { + primaryPhoneNumber: '+1', + primaryPhoneCountryCode: '234-567-890', + additionalPhones: [], + }, linkedinLink: { primaryLinkUrl: 'https://www.linkedin.com', primaryLinkLabel: 'linkedin', @@ -52,7 +56,11 @@ const mockPerson = { }, performanceRating: 1, createdAt: '2021-08-03T19:20:06.000Z', - phone: 'phone', + phone: { + primaryPhoneNumber: '+1', + primaryPhoneCountryCode: '234-567-890', + additionalPhones: [], + }, id: '123', city: 'city', companyId: '1', @@ -80,7 +88,11 @@ const mocks: MockedResponse[] = [ __typename updatedAt myCustomObjectId - whatsapp + whatsapp { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } linkedinLink { primaryLinkUrl primaryLinkLabel @@ -106,7 +118,11 @@ const mocks: MockedResponse[] = [ } performanceRating createdAt - phone + phone { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } id city companyId 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 52c11a204..ce0253dda 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 @@ -662,7 +662,10 @@ export const mockPerformance = { }, id: '20202020-2d40-4e49-8df4-9c6a049191df', email: 'lorie.vladim@google.com', - phone: '+33788901235', + phones: { + primaryPhoneCountryCode: '+33', + primaryPhoneNumber: '788901235', + }, linkedinLink: { __typename: 'Link', primaryLinkLabel: '', 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 17e9d27f4..deee867fc 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 @@ -56,16 +56,27 @@ export const PhonesDisplay = ({ value, isFocused }: PhonesDisplayProps) => { ], ); + const parsePhoneNumberOrReturnInvalidValue = (number: string) => { + try { + return { parsedPhone: parsePhoneNumber(number) }; + } catch (e) { + return { invalidPhone: number }; + } + }; + return isFocused ? ( {phones.map(({ number, countryCode }, index) => { - const parsedPhone = parsePhoneNumber(countryCode + number); - const URI = parsedPhone.getURI(); + const { parsedPhone, invalidPhone } = + parsePhoneNumberOrReturnInvalidValue(countryCode + number); + const URI = parsedPhone?.getURI(); return ( ); })} @@ -73,13 +84,16 @@ export const PhonesDisplay = ({ value, isFocused }: PhonesDisplayProps) => { ) : ( {phones.map(({ number, countryCode }, index) => { - const parsedPhone = parsePhoneNumber(countryCode + number); - const URI = parsedPhone.getURI(); + const { parsedPhone, invalidPhone } = + parsePhoneNumberOrReturnInvalidValue(countryCode + number); + const URI = parsedPhone?.getURI(); return ( ); })} diff --git a/packages/twenty-front/src/testing/mock-data/people.ts b/packages/twenty-front/src/testing/mock-data/people.ts index 439730e43..9139aa194 100644 --- a/packages/twenty-front/src/testing/mock-data/people.ts +++ b/packages/twenty-front/src/testing/mock-data/people.ts @@ -45,7 +45,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { __typename: 'Person', createdAt: '2024-08-02T09:52:46.814Z', city: 'ASd', - phone: '', + phones: { + primaryPhoneNumber: '781234562', + primaryPhoneCountryCode: '+33', + }, id: 'da3c2c4b-da01-4b81-9734-226069eb4cd0', jobTitle: '', position: 0, @@ -172,7 +175,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { __typename: 'Person', createdAt: '2024-08-01T09:50:00.000Z', city: 'Seattle', - phone: '+33789012345', + phones: { + primaryPhoneNumber: '781234562', + primaryPhoneCountryCode: '+33', + }, id: '20202020-1c0e-494c-a1b6-85b1c6fefaa5', jobTitle: '', position: 1, @@ -299,7 +305,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { __typename: 'Person', createdAt: '2024-08-02T09:48:36.193Z', city: 'Los Angeles', - phone: '+33780123456', + phones: { + primaryPhoneNumber: '781234576', + primaryPhoneCountryCode: '+33', + }, id: '20202020-ac73-4797-824e-87a1f5aea9e0', jobTitle: '', position: 2, @@ -395,7 +404,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { __typename: 'Person', createdAt: '2024-08-02T09:48:36.193Z', city: 'Seattle', - phone: '+33789012345', + phones: { + primaryPhoneNumber: '781234545', + primaryPhoneCountryCode: '+33', + }, id: '20202020-f517-42fd-80ae-14173b3b70ae', jobTitle: '', position: 3, @@ -491,7 +503,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { __typename: 'Person', createdAt: '2024-08-02T09:48:36.193Z', city: 'Los Angeles', - phone: '+33780123456', + phones: { + primaryPhoneNumber: '781234587', + primaryPhoneCountryCode: '+33', + }, id: '20202020-eee1-4690-ad2c-8619e5b56a2e', jobTitle: '', position: 4, @@ -587,7 +602,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { __typename: 'Person', createdAt: '2024-08-02T09:48:36.193Z', city: 'Seattle', - phone: '+33781234567', + phones: { + primaryPhoneNumber: '781234599', + primaryPhoneCountryCode: '+33', + }, id: '20202020-6784-4449-afdf-dc62cb8702f2', jobTitle: '', position: 5, @@ -683,7 +701,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { __typename: 'Person', createdAt: '2024-08-02T09:48:36.193Z', city: 'New York', - phone: '+33782345678', + phones: { + primaryPhoneNumber: '781234572', + primaryPhoneCountryCode: '+33', + }, id: '20202020-490f-4466-8391-733cfd66a0c8', jobTitle: '', position: 6, @@ -779,7 +800,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { __typename: 'Person', createdAt: '2024-08-02T09:48:36.193Z', city: 'Seattle', - phone: '+33783456789', + phones: { + primaryPhoneNumber: '781234582', + primaryPhoneCountryCode: '+33', + }, id: '20202020-80f1-4dff-b570-a74942528de3', jobTitle: '', position: 7, @@ -875,7 +899,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { __typename: 'Person', createdAt: '2024-08-02T09:48:36.193Z', city: 'New York', - phone: '+33784567890', + phones: { + primaryPhoneNumber: '781234569', + primaryPhoneCountryCode: '+33', + }, id: '20202020-338b-46df-8811-fa08c7d19d35', jobTitle: '', position: 8, @@ -971,7 +998,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { __typename: 'Person', createdAt: '2024-08-02T09:48:36.193Z', city: 'San Francisco', - phone: '+33785678901', + phones: { + primaryPhoneNumber: '781234962', + primaryPhoneCountryCode: '+33', + }, id: '20202020-64ad-4b0e-bbfd-e9fd795b7016', jobTitle: '', position: 9, @@ -1067,7 +1097,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { __typename: 'Person', createdAt: '2024-08-02T09:48:36.193Z', city: 'New York', - phone: '+33786789012', + phones: { + primaryPhoneNumber: '781234502', + primaryPhoneCountryCode: '+33', + }, id: '20202020-5d54-41b7-ba36-f0d20e1417ae', jobTitle: '', position: 10, @@ -1163,7 +1196,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { __typename: 'Person', createdAt: '2024-08-02T09:48:36.193Z', city: 'Los Angeles', - phone: '+33787890123', + phones: { + primaryPhoneNumber: '781234563', + primaryPhoneCountryCode: '+33', + }, id: '20202020-623d-41fe-92e7-dd45b7c568e1', jobTitle: '', position: 11, @@ -1259,7 +1295,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { __typename: 'Person', createdAt: '2024-08-02T09:48:36.193Z', city: 'Seattle', - phone: '+33788901234', + phones: { + primaryPhoneNumber: '781234542', + primaryPhoneCountryCode: '+33', + }, id: '20202020-2d40-4e49-8df4-9c6a049190ef', jobTitle: '', position: 12, @@ -1355,7 +1394,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { __typename: 'Person', createdAt: '2024-08-02T09:48:36.193Z', city: 'Seattle', - phone: '+33788901234', + phones: { + primaryPhoneNumber: '782234562', + primaryPhoneCountryCode: '+33', + }, id: '20202020-2d40-4e49-8df4-9c6a049190df', jobTitle: '', position: 13, @@ -1451,7 +1493,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { __typename: 'Person', createdAt: '2024-08-02T09:48:36.193Z', city: 'Seattle', - phone: '+33788901234', + phones: { + primaryPhoneNumber: '781274562', + primaryPhoneCountryCode: '+33', + }, id: '20202020-2d40-4e49-8df4-9c6a049191de', jobTitle: '', position: 14, @@ -1547,7 +1592,10 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { __typename: 'Person', createdAt: '2024-08-02T09:48:36.193Z', city: 'Seattle', - phone: '+33788901235', + phones: { + primaryPhoneNumber: '781239562', + primaryPhoneCountryCode: '+33', + }, id: '20202020-2d40-4e49-8df4-9c6a049191df', jobTitle: '', position: 15, 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 9d98d33ab..e390ab162 100644 --- a/packages/twenty-server/src/database/commands/database-command.module.ts +++ b/packages/twenty-server/src/database/commands/database-command.module.ts @@ -9,6 +9,7 @@ import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-wo import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question'; import { UpgradeTo0_30CommandModule } from 'src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module'; import { UpgradeTo0_31CommandModule } from 'src/database/commands/upgrade-version/0-31/0-31-upgrade-version.module'; +import { UpgradeTo0_32CommandModule } from 'src/database/commands/upgrade-version/0-32/0-32-upgrade-version.module'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; @@ -50,6 +51,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp UpgradeTo0_30CommandModule, UpgradeTo0_31CommandModule, FeatureFlagModule, + UpgradeTo0_32CommandModule, ], providers: [ DataSeedWorkspaceCommand, diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-migrate-phone-fields-to-phones.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-migrate-phone-fields-to-phones.command.ts new file mode 100644 index 000000000..d5c100333 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-migrate-phone-fields-to-phones.command.ts @@ -0,0 +1,338 @@ +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import { isDefined, isEmpty } from 'class-validator'; +import { parsePhoneNumber } from 'libphonenumber-js'; +import { Command } from 'nest-commander'; +import { DataSource, QueryRunner, Repository } from 'typeorm'; + +import { + ActiveWorkspacesCommandOptions, + ActiveWorkspacesCommandRunner, +} from 'src/database/commands/active-workspaces.command'; +import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; +import { + FieldMetadataEntity, + FieldMetadataType, +} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service'; +import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { computeTableName } from 'src/engine/utils/compute-table-name.util'; +import { PERSON_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { ViewService } from 'src/modules/view/services/view.service'; +import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; + +type MigratePhoneFieldsToPhonesCommandOptions = ActiveWorkspacesCommandOptions; +@Command({ + name: 'upgrade-0.32:migrate-phone-fields-to-phones', + description: 'Migrating fields of deprecated type PHONE to type PHONES', +}) +export class MigratePhoneFieldsToPhonesCommand extends ActiveWorkspacesCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + @InjectDataSource('metadata') + private readonly metadataDataSource: DataSource, + private readonly fieldMetadataService: FieldMetadataService, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly typeORMService: TypeORMService, + private readonly dataSourceService: DataSourceService, + private readonly viewService: ViewService, + ) { + super(workspaceRepository); + } + + async executeActiveWorkspacesCommand( + _passedParam: string[], + _options: MigratePhoneFieldsToPhonesCommandOptions, + workspaceIds: string[], + ): Promise { + this.logger.log( + 'Running command to migrate phone type fields to phones type', + ); + for (const workspaceId of workspaceIds) { + this.logger.log(`Running command for workspace ${workspaceId}`); + try { + const dataSourceMetadata = + await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId( + workspaceId, + ); + + if (!dataSourceMetadata) { + throw new Error( + `Could not find dataSourceMetadata for workspace ${workspaceId}`, + ); + } + const workspaceDataSource = + await this.typeORMService.connectToDataSource(dataSourceMetadata); + + if (!workspaceDataSource) { + throw new Error( + `Could not connect to dataSource for workspace ${workspaceId}`, + ); + } + const standardPersonPhoneFieldWithTextType = + await this.fieldMetadataRepository.findOneBy({ + workspaceId, + standardId: PERSON_STANDARD_FIELD_IDS.phone, + }); + + if (!standardPersonPhoneFieldWithTextType) { + throw new Error( + `Could not find standard phone field on person for workspace ${workspaceId}`, + ); + } + + await this.migrateStandardPersonPhoneField({ + standardPersonPhoneField: standardPersonPhoneFieldWithTextType, + workspaceDataSource, + workspaceSchemaName: dataSourceMetadata.schema, + }); + + const fieldsWithPhoneType = await this.fieldMetadataRepository.find({ + where: { + workspaceId, + type: FieldMetadataType.PHONE, + }, + }); + + for (const deprecatedPhoneField of fieldsWithPhoneType) { + await this.migrateCustomPhoneField({ + phoneField: deprecatedPhoneField, + workspaceDataSource, + workspaceSchemaName: dataSourceMetadata.schema, + }); + } + } catch (error) { + this.logger.log( + chalk.red( + `Field migration on workspace ${workspaceId} failed with error: ${error}`, + ), + ); + continue; + } + this.logger.log(chalk.green(`Command completed!`)); + } + } + + private async migrateStandardPersonPhoneField({ + standardPersonPhoneField, + workspaceDataSource, + workspaceSchemaName, + }: { + standardPersonPhoneField: FieldMetadataEntity; + workspaceDataSource: DataSource; + workspaceSchemaName: string; + }) { + const personObjectMetadata = await this.objectMetadataRepository.findOne({ + where: { id: standardPersonPhoneField.objectMetadataId }, + }); + + if (!personObjectMetadata) { + throw new Error( + `Could not find Person objectMetadata (id ${standardPersonPhoneField.objectMetadataId})`, + ); + } + + this.logger.log(`Attempting to migrate standard person phone field.`); + const workspaceQueryRunner = workspaceDataSource.createQueryRunner(); + + await workspaceQueryRunner.connect(); + const { id: _id, ...deprecatedPhoneFieldWithoutId } = + standardPersonPhoneField; + + const workspaceId = standardPersonPhoneField.workspaceId; + + try { + let standardPersonPhonesFieldType = + await this.fieldMetadataRepository.findOneBy({ + workspaceId, + standardId: PERSON_STANDARD_FIELD_IDS.phones, + }); + + if (!standardPersonPhonesFieldType) { + standardPersonPhonesFieldType = + await this.fieldMetadataService.createOne({ + ...deprecatedPhoneFieldWithoutId, + type: FieldMetadataType.PHONES, + defaultValue: null, + name: 'phones', + } satisfies CreateFieldInput); + } + + // Copy phone data from Text type to Phones type + await this.copyAndParseDeprecatedPhoneFieldDataIntoNewPhonesField({ + workspaceQueryRunner, + workspaceSchemaName, + }); + + // Add new phones field to views and hide deprecated phone field + const viewFieldRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'viewField', + ); + const viewFieldsWithDeprecatedPhoneField = await viewFieldRepository.find( + { + where: { + fieldMetadataId: standardPersonPhoneField.id, + isVisible: true, + }, + }, + ); + + await this.viewService.addFieldToViews({ + workspaceId: workspaceId, + fieldId: standardPersonPhonesFieldType.id, + viewsIds: viewFieldsWithDeprecatedPhoneField + .filter((viewField) => viewField.viewId !== null) + .map((viewField) => viewField.viewId as string), + positions: viewFieldsWithDeprecatedPhoneField.reduce( + (acc, viewField) => { + if (!viewField.viewId) { + return acc; + } + acc[viewField.viewId] = viewField.position; + + return acc; + }, + [], + ), + }); + + await this.viewService.removeFieldFromViews({ + workspaceId: workspaceId, + fieldId: standardPersonPhoneField.id, + }); + + this.logger.log( + `Migration of standard person phone field to phones is done!`, + ); + } catch (error) { + this.logger.log( + chalk.red( + `Failed to migrate field standard person phone field to phones, rolling back. (Error: ${error})`, + ), + ); + + // Delete new phones field if it was created + const newPhonesField = + await this.fieldMetadataService.findOneWithinWorkspace(workspaceId, { + where: { + name: 'phones', + objectMetadataId: standardPersonPhoneField.objectMetadataId, + }, + }); + + if (newPhonesField) { + this.logger.log( + `Deleting phones field of type Phone as part of rolling back.`, + ); + await this.fieldMetadataService.deleteOneField( + { id: newPhonesField.id }, + workspaceId, + ); + } + } finally { + await workspaceQueryRunner.release(); + } + } + + private async migrateCustomPhoneField({ + phoneField, + workspaceDataSource, + workspaceSchemaName, + }: { + phoneField: FieldMetadataEntity; + workspaceDataSource: DataSource; + workspaceSchemaName: string; + }) { + if (!phoneField) return; + const objectMetadata = await this.objectMetadataRepository.findOne({ + where: { id: phoneField.objectMetadataId }, + }); + + if (!objectMetadata) { + throw new Error( + `Could not find objectMetadata for field ${phoneField.name}`, + ); + } + this.logger.log( + `Attempting to migrate field ${phoneField.name} on ${objectMetadata.nameSingular} from Phone to Text.`, + ); + const workspaceQueryRunner = workspaceDataSource.createQueryRunner(); + + await workspaceQueryRunner.connect(); + + try { + await this.metadataDataSource.query( + `UPDATE "metadata"."fieldMetadata" SET "type" = $1 where "id"=$2`, + [FieldMetadataType.TEXT, phoneField.id], + ); + + await workspaceQueryRunner.query( + `ALTER TABLE "${workspaceSchemaName}"."${computeTableName(objectMetadata.nameSingular, objectMetadata.isCustom)}" ALTER COLUMN "${computeColumnName(phoneField.name)}" TYPE TEXT`, + ); + } catch (error) { + this.logger.log( + chalk.red( + `Failed to migrate field ${phoneField.name} on ${objectMetadata.nameSingular} from Phone to Text.`, + ), + ); + } finally { + await workspaceQueryRunner.release(); + } + } + + private async copyAndParseDeprecatedPhoneFieldDataIntoNewPhonesField({ + workspaceQueryRunner, + workspaceSchemaName, + }: { + workspaceQueryRunner: QueryRunner; + workspaceSchemaName: string; + }) { + const deprecatedPhoneFieldRows = await workspaceQueryRunner.query( + `SELECT id, phone FROM "${workspaceSchemaName}"."person" WHERE + phone IS NOT null`, + ); + + for (const row of deprecatedPhoneFieldRows) { + const phoneColumnValue = row['phone']; + + if (isDefined(phoneColumnValue) && !isEmpty(phoneColumnValue)) { + const query = `UPDATE "${workspaceSchemaName}"."person" SET "phonesPrimaryPhoneCountryCode" = $1,"phonesPrimaryPhoneNumber" = $2 where "id"=$3 AND ("phonesPrimaryPhoneCountryCode" IS NULL OR "phonesPrimaryPhoneCountryCode" = '');`; + + try { + const parsedPhoneColumnValue = parsePhoneNumber(phoneColumnValue); + + await workspaceQueryRunner.query(query, [ + `+${parsedPhoneColumnValue.countryCallingCode}`, + parsedPhoneColumnValue.nationalNumber, + row.id, + ]); + } catch (error) { + this.logger.log( + chalk.red( + `Could not save phone number ${phoneColumnValue}, will try again storing value as is without parsing, with default country code.`, + ), + ); + // Store the invalid string for invalid phone numbers + await workspaceQueryRunner.query(query, [ + '', + phoneColumnValue, + row.id, + ]); + } + } + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.command.ts new file mode 100644 index 000000000..dd1a6db03 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.command.ts @@ -0,0 +1,46 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { Repository } from 'typeorm'; + +import { + ActiveWorkspacesCommandOptions, + ActiveWorkspacesCommandRunner, +} from 'src/database/commands/active-workspaces.command'; +import { MigratePhoneFieldsToPhonesCommand } from 'src/database/commands/upgrade-version/0-32/0-32-migrate-phone-fields-to-phones.command'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command'; + +type UpdateTo0_32CommandOptions = ActiveWorkspacesCommandOptions; + +@Command({ + name: 'upgrade-0.32', + description: 'Upgrade to 0.32', +}) +export class UpgradeTo0_32Command extends ActiveWorkspacesCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand, + private readonly migratePhoneFieldsToPhones: MigratePhoneFieldsToPhonesCommand, + ) { + super(workspaceRepository); + } + + async executeActiveWorkspacesCommand( + _passedParam: string[], + options: UpdateTo0_32CommandOptions, + workspaceIds: string[], + ): Promise { + await this.syncWorkspaceMetadataCommand.executeActiveWorkspacesCommand( + _passedParam, + { ...options, force: true }, + workspaceIds, + ); + await this.migratePhoneFieldsToPhones.executeActiveWorkspacesCommand( + _passedParam, + options, + workspaceIds, + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.module.ts new file mode 100644 index 000000000..cabf5ef86 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.module.ts @@ -0,0 +1,32 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { MigratePhoneFieldsToPhonesCommand } from 'src/database/commands/upgrade-version/0-32/0-32-migrate-phone-fields-to-phones.command'; +import { UpgradeTo0_32Command } from 'src/database/commands/upgrade-version/0-32/0-32-upgrade-version.command'; +import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; +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 { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; +import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module'; +import { ViewModule } from 'src/modules/view/view.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Workspace], 'core'), + WorkspaceSyncMetadataCommandsModule, + DataSourceModule, + WorkspaceMetadataVersionModule, + FieldMetadataModule, + TypeOrmModule.forFeature( + [FieldMetadataEntity, ObjectMetadataEntity], + 'metadata', + ), + TypeORMModule, + ViewModule, + ], + providers: [UpgradeTo0_32Command, MigratePhoneFieldsToPhonesCommand], +}) +export class UpgradeTo0_32CommandModule {} 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 ec8135154..330975f00 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/metadata/fieldsMetadata.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/metadata/fieldsMetadata.ts @@ -92,7 +92,7 @@ export const getDevSeedPeopleCustomFields = ( }, { workspaceId, - type: FieldMetadataType.PHONE, + type: FieldMetadataType.PHONES, name: 'whatsapp', label: 'Whatsapp', description: "Contact's Whatsapp Number", 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 6624fd503..22adfe014 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/workspace/people.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/workspace/people.ts @@ -34,12 +34,14 @@ export const seedPeople = async ( 'id', 'nameFirstName', 'nameLastName', - 'phone', + 'phonesPrimaryPhoneCountryCode', + 'phonesPrimaryPhoneNumber', 'city', 'companyId', 'emailsPrimaryEmail', 'position', - 'whatsapp', + 'whatsappPrimaryPhoneCountryCode', + 'whatsappPrimaryPhoneNumber', 'createdBySource', 'createdByWorkspaceMemberId', 'createdByName', @@ -50,12 +52,14 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.CHRISTOPH, nameFirstName: 'Christoph', nameLastName: 'Callisto', - phone: '+33789012345', + phonePrimaryPhoneCountryCode: '+33', + phonePrimaryPhoneNumber: '789012345', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.LINKEDIN, emailsPrimaryEmail: 'christoph.calisto@linkedin.com', position: 1, - whatsapp: '+33789012345', + whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneNumber: '789012345', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByName: 'Tim Apple', @@ -64,12 +68,14 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.SYLVIE, nameFirstName: 'Sylvie', nameLastName: 'Palmer', - phone: '+33780123456', + phonePrimaryPhoneCountryCode: '+33', + phonePrimaryPhoneNumber: '780123456', city: 'Los Angeles', companyId: DEV_SEED_COMPANY_IDS.LINKEDIN, emailsPrimaryEmail: 'sylvie.palmer@linkedin.com', position: 2, - whatsapp: '+33780123456', + whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneNumber: '780123456', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByName: 'Tim Apple', @@ -78,12 +84,14 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.CHRISTOPHER_G, nameFirstName: 'Christopher', nameLastName: 'Gonzalez', - phone: '+33789012345', + phonePrimaryPhoneCountryCode: '+33', + phonePrimaryPhoneNumber: '789012345', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.QONTO, emailsPrimaryEmail: 'christopher.gonzalez@qonto.com', position: 3, - whatsapp: '+33789012345', + whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneNumber: '789012345', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByName: 'Tim Apple', @@ -92,12 +100,14 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.ASHLEY, nameFirstName: 'Ashley', nameLastName: 'Parker', - phone: '+33780123456', + phonePrimaryPhoneCountryCode: '+33', + phonePrimaryPhoneNumber: '780123456', city: 'Los Angeles', companyId: DEV_SEED_COMPANY_IDS.QONTO, emailsPrimaryEmail: 'ashley.parker@qonto.com', position: 4, - whatsapp: '+33780123456', + whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneNumber: '780123456', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByName: 'Tim Apple', @@ -106,12 +116,14 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.NICHOLAS, nameFirstName: 'Nicholas', nameLastName: 'Wright', - phone: '+33781234567', + phonePrimaryPhoneCountryCode: '+33', + phonePrimaryPhoneNumber: '781234567', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.MICROSOFT, emailsPrimaryEmail: 'nicholas.wright@microsoft.com', position: 5, - whatsapp: '+33781234567', + whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneNumber: '781234567', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByName: 'Tim Apple', @@ -120,12 +132,14 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.ISABELLA, nameFirstName: 'Isabella', nameLastName: 'Scott', - phone: '+33782345678', + phonePrimaryPhoneCountryCode: '+33', + phonePrimaryPhoneNumber: '782345678', city: 'New York', companyId: DEV_SEED_COMPANY_IDS.MICROSOFT, emailsPrimaryEmail: 'isabella.scott@microsoft.com', position: 6, - whatsapp: '+33782345678', + whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneNumber: '782345678', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByName: 'Tim Apple', @@ -134,12 +148,14 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.MATTHEW, nameFirstName: 'Matthew', nameLastName: 'Green', - phone: '+33783456789', + phonePrimaryPhoneCountryCode: '+33', + phonePrimaryPhoneNumber: '783456789', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.MICROSOFT, emailsPrimaryEmail: 'matthew.green@microsoft.com', position: 7, - whatsapp: '+33783456789', + whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneNumber: '783456789', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByName: 'Tim Apple', @@ -148,12 +164,14 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.ELIZABETH, nameFirstName: 'Elizabeth', nameLastName: 'Baker', - phone: '+33784567890', + phonePrimaryPhoneCountryCode: '+33', + phonePrimaryPhoneNumber: '784567890', city: 'New York', companyId: DEV_SEED_COMPANY_IDS.AIRBNB, emailsPrimaryEmail: 'elizabeth.baker@airbnb.com', position: 8, - whatsapp: '+33784567890', + whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneNumber: '784567890', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByName: 'Tim Apple', @@ -162,12 +180,14 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.CHRISTOPHER_N, nameFirstName: 'Christopher', nameLastName: 'Nelson', - phone: '+33785678901', + phonePrimaryPhoneCountryCode: '+33', + phonePrimaryPhoneNumber: '785678901', city: 'San Francisco', companyId: DEV_SEED_COMPANY_IDS.AIRBNB, emailsPrimaryEmail: 'christopher.nelson@airbnb.com', position: 9, - whatsapp: '+33785678901', + whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneNumber: '785678901', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByName: 'Tim Apple', @@ -176,12 +196,14 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.AVERY, nameFirstName: 'Avery', nameLastName: 'Carter', - phone: '+33786789012', + phonePrimaryPhoneCountryCode: '+33', + phonePrimaryPhoneNumber: '786789012', city: 'New York', companyId: DEV_SEED_COMPANY_IDS.AIRBNB, emailsPrimaryEmail: 'avery.carter@airbnb.com', position: 10, - whatsapp: '+33786789012', + whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneNumber: '786789012', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByName: 'Tim Apple', @@ -190,12 +212,14 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.ETHAN, nameFirstName: 'Ethan', nameLastName: 'Mitchell', - phone: '+33787890123', + phonePrimaryPhoneCountryCode: '+33', + phonePrimaryPhoneNumber: '787890123', city: 'Los Angeles', companyId: DEV_SEED_COMPANY_IDS.GOOGLE, emailsPrimaryEmail: 'ethan.mitchell@google.com', position: 11, - whatsapp: '+33787890123', + whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneNumber: '787890123', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByName: 'Tim Apple', @@ -204,12 +228,14 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.MADISON, nameFirstName: 'Madison', nameLastName: 'Perez', - phone: '+33788901234', + phonePrimaryPhoneCountryCode: '+33', + phonePrimaryPhoneNumber: '788901234', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.GOOGLE, emailsPrimaryEmail: 'madison.perez@google.com', position: 12, - whatsapp: '+33788901234', + whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneNumber: '788901234', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByName: 'Tim Apple', @@ -218,12 +244,14 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.BERTRAND, nameFirstName: 'Bertrand', nameLastName: 'Voulzy', - phone: '+33788901234', + phonePrimaryPhoneCountryCode: '+33', + phonePrimaryPhoneNumber: '788901234', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.GOOGLE, emailsPrimaryEmail: 'bertrand.voulzy@google.com', position: 13, - whatsapp: '+33788901234', + whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneNumber: '788901234', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByName: 'Tim Apple', @@ -232,12 +260,14 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.LOUIS, nameFirstName: 'Louis', nameLastName: 'Duss', - phone: '+33788901234', + phonePrimaryPhoneCountryCode: '+33', + phonePrimaryPhoneNumber: '789012345', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.GOOGLE, emailsPrimaryEmail: 'louis.duss@google.com', position: 14, - whatsapp: '+33788901234', + whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneNumber: '789012345', createdBySource: 'MANUAL', createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, createdByName: 'Tim Apple', @@ -246,12 +276,14 @@ export const seedPeople = async ( id: DEV_SEED_PERSON_IDS.LORIE, nameFirstName: 'Lorie', nameLastName: 'Vladim', - phone: '+33788901235', + phonePrimaryPhoneCountryCode: '+33', + phonePrimaryPhoneNumber: '788901235', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.GOOGLE, emailsPrimaryEmail: 'lorie.vladim@google.com', position: 15, - whatsapp: '+33788901235', + whatsappPrimaryPhoneCountryCode: '+33', + whatsappPrimaryPhoneNumber: '788901235', createdBySource: 'MANUAL', createdByWorkspaceMemberId: null, createdByName: '', diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/company.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/company.ts index 8d5a24a1e..2f3966e10 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/company.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/company.ts @@ -1,5 +1,11 @@ import { EntityManager } from 'typeorm'; +export const AIRBNB_ID = 'c776ee49-f608-4a77-8cc8-6fe96ae1e43f'; +export const QONTO_ID = 'f45ee421-8a3e-4aa5-a1cf-7207cc6754e1'; +export const STRIPE_ID = '1f70157c-4ea5-4d81-bc49-e1401abfbb94'; +export const FIGMA_ID = '9d5bcf43-7d38-4e88-82cb-d6d4ce638bf0'; +export const NOTION_ID = '06290608-8bf0-4806-99ae-a715a6a93fad'; + export const companyPrefillData = async ( entityManager: EntityManager, schemaName: string, @@ -8,6 +14,7 @@ export const companyPrefillData = async ( .createQueryBuilder() .insert() .into(`${schemaName}.company`, [ + 'id', 'name', 'domainNamePrimaryLinkUrl', 'addressAddressStreet1', @@ -25,6 +32,7 @@ export const companyPrefillData = async ( .orIgnore() .values([ { + id: AIRBNB_ID, name: 'Airbnb', domainNamePrimaryLinkUrl: 'https://airbnb.com', addressAddressStreet1: '888 Brannan St', @@ -40,6 +48,7 @@ export const companyPrefillData = async ( createdByName: 'System', }, { + id: QONTO_ID, name: 'Qonto', domainNamePrimaryLinkUrl: 'https://qonto.com', addressAddressStreet1: '18 rue de navarrin', @@ -55,6 +64,7 @@ export const companyPrefillData = async ( createdByName: 'System', }, { + id: STRIPE_ID, name: 'Stripe', domainNamePrimaryLinkUrl: 'https://stripe.com', addressAddressStreet1: 'Eutaw Street', @@ -70,6 +80,7 @@ export const companyPrefillData = async ( createdByName: 'System', }, { + id: FIGMA_ID, name: 'Figma', domainNamePrimaryLinkUrl: 'https://figma.com', addressAddressStreet1: '760 Market St', @@ -85,6 +96,7 @@ export const companyPrefillData = async ( createdByName: 'System', }, { + id: NOTION_ID, name: 'Notion', domainNamePrimaryLinkUrl: 'https://notion.com', addressAddressStreet1: '2300 Harrison St', diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/person.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/person.ts index fb227b15c..ec07c61f1 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/person.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/person.ts @@ -1,5 +1,13 @@ import { EntityManager } from 'typeorm'; +import { + AIRBNB_ID, + FIGMA_ID, + NOTION_ID, + QONTO_ID, + STRIPE_ID, +} from 'src/engine/workspace-manager/standard-objects-prefill-data/company'; + // FixMe: Is this file a duplicate of src/database/typeorm-seeds/workspace/people.ts export const personPrefillData = async ( entityManager: EntityManager, @@ -18,6 +26,9 @@ export const personPrefillData = async ( 'createdBySource', 'createdByWorkspaceMemberId', 'createdByName', + 'phonesPrimaryPhoneNumber', + 'phonesPrimaryPhoneCountryCode', + 'companyId', ]) .orIgnore() .values([ @@ -32,6 +43,9 @@ export const personPrefillData = async ( createdBySource: 'MANUAL', createdByWorkspaceMemberId: null, createdByName: 'System', + phonesPrimaryPhoneNumber: '1234567890', + phonesPrimaryPhoneCountryCode: '+1', + companyId: AIRBNB_ID, }, { nameFirstName: 'Alexandre', @@ -44,6 +58,9 @@ export const personPrefillData = async ( createdBySource: 'MANUAL', createdByWorkspaceMemberId: null, createdByName: 'System', + phonesPrimaryPhoneNumber: '677118822', + phonesPrimaryPhoneCountryCode: '+33', + companyId: QONTO_ID, }, { nameFirstName: 'Patrick', @@ -56,6 +73,9 @@ export const personPrefillData = async ( createdBySource: 'MANUAL', createdByWorkspaceMemberId: null, createdByName: 'System', + phonesPrimaryPhoneNumber: '987625341', + phonesPrimaryPhoneCountryCode: '+1', + companyId: STRIPE_ID, }, { nameFirstName: 'Dylan', @@ -68,6 +88,9 @@ export const personPrefillData = async ( createdBySource: 'MANUAL', createdByWorkspaceMemberId: null, createdByName: 'System', + phonesPrimaryPhoneNumber: '09882261', + phonesPrimaryPhoneCountryCode: '+1', + companyId: FIGMA_ID, }, { nameFirstName: 'Ivan', @@ -80,6 +103,9 @@ export const personPrefillData = async ( createdBySource: 'MANUAL', createdByWorkspaceMemberId: null, createdByName: 'System', + phonesPrimaryPhoneNumber: '88226173', + phonesPrimaryPhoneCountryCode: '+1', + companyId: NOTION_ID, }, ]) .returning('*') diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/people-all.view.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/people-all.view.ts index 6beb5cda8..9fbddef91 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/people-all.view.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/people-all.view.ts @@ -57,7 +57,7 @@ export const peopleAllView = async ( { fieldMetadataId: objectMetadataMap[STANDARD_OBJECT_IDS.person].fields[ - PERSON_STANDARD_FIELD_IDS.phone + PERSON_STANDARD_FIELD_IDS.phones ], position: 4, isVisible: true, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index 8f0d135fd..8a19d49e4 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -310,6 +310,7 @@ export const PERSON_STANDARD_FIELD_IDS = { xLink: '20202020-8fc2-487c-b84a-55a99b145cfd', jobTitle: '20202020-b0d0-415a-bef9-640a26dacd9b', phone: '20202020-4564-4b8b-a09f-05445f2e0bce', + phones: '34becd3e-3c51-43fa-8b6e-af39e29368ab', city: '20202020-5243-4ffb-afc5-2c675da41346', avatarUrl: '20202020-b8a6-40df-961c-373dc5d2ec21', position: '20202020-fcd5-4231-aff5-fff583eaa0b1', diff --git a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts index a39a401ca..e162bb82b 100644 --- a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts +++ b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts @@ -7,6 +7,7 @@ import { import { EmailsMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type'; import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type'; import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type'; +import { PhonesMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType, @@ -109,8 +110,18 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { description: 'Contact’s phone number', icon: 'IconPhone', }) + @WorkspaceIsDeprecated() phone: string; + @WorkspaceField({ + standardId: PERSON_STANDARD_FIELD_IDS.phones, + type: FieldMetadataType.PHONES, + label: 'Phones', + description: 'Contact’s phone numbers', + icon: 'IconPhone', + }) + phones: PhonesMetadata; + @WorkspaceField({ standardId: PERSON_STANDARD_FIELD_IDS.city, type: FieldMetadataType.TEXT, diff --git a/packages/twenty-server/test/people.integration-spec.ts b/packages/twenty-server/test/people.integration-spec.ts index b96bb6700..fd568bd5b 100644 --- a/packages/twenty-server/test/people.integration-spec.ts +++ b/packages/twenty-server/test/people.integration-spec.ts @@ -11,7 +11,10 @@ describe('peopleResolver (integration)', () => { edges { node { jobTitle - phone + phones { + primaryPhoneNumber + primaryPhoneCountryCode + } city avatarUrl position @@ -21,7 +24,9 @@ describe('peopleResolver (integration)', () => { deletedAt companyId intro - whatsapp + whatsapp { + primaryPhoneNumber + } workPrefereance performanceRating } @@ -37,6 +42,7 @@ describe('peopleResolver (integration)', () => { .send(queryData) .expect(200) .expect((res) => { + console.log(res.body); expect(res.body.data).toBeDefined(); expect(res.body.errors).toBeUndefined(); }) @@ -52,7 +58,7 @@ describe('peopleResolver (integration)', () => { const people = edges[0].node; expect(people).toHaveProperty('jobTitle'); - expect(people).toHaveProperty('phone'); + expect(people).toHaveProperty('phones'); expect(people).toHaveProperty('city'); expect(people).toHaveProperty('avatarUrl'); expect(people).toHaveProperty('position');