From fa241fa4e987239d2f74d16704a3880c6007ae19 Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Tue, 24 Sep 2024 16:31:30 +0200 Subject: [PATCH] Handle migration of Phone field to Phones field (#7128) This PR was created by [GitStart](https://gitstart.com/) to address the requirements from this ticket: [TWNTY-6260](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-6260). This ticket was imported from: [TWNTY-6260](https://github.com/twentyhq/twenty/issues/6260) --- ### Description This is the second PR on TWNTY-6260 which handles data migration of Phone field to Phones field.\ \ How to Test?\ Follow the below steps: - On the main branch, - go to `packages/twenty-server/src/database/typeorm-seeds/workspace/people.ts` and change any person's phone number to a string with characters for example: "test invalid phone", and then reset the DB. - reset database using `npx nx database:reset twenty-server` - This is to make sure that invalid numbers will be handled properly. We should use the invalid value itself to avoid removing data and see how the behavior is on the front end. should be the same as in the main, the display shows the invalid value, but the input is empty when you click, and then you can update. - Checkout to `TWNTY-6260-phone-migration` branch - Rebuild typescript using `npx nx build twenty-server` - Run command `yarn command:prod upgrade-0.32` to do migration - Run both backend and frontend to see the migrated field ### Demo - **Loom Video:**\ ### Refs #6260 --------- Co-authored-by: gitstart-twenty Co-authored-by: Marie Stoppa Co-authored-by: Weiko --- .../mapFieldMetadataToGraphQLQuery.test.tsx | 4 + .../mapObjectMetadataToGraphQLQuery.test.tsx | 6 +- .../utils/getObjectMetadataItemsMock.ts | 4 +- .../hooks/__mocks__/personFragment.ts | 12 +- .../hooks/__mocks__/useCreateManyRecords.ts | 5 +- .../hooks/__mocks__/useCreateOneRecord.ts | 5 +- .../hooks/__mocks__/useUpdateOneRecord.ts | 1 - .../__tests__/useUpdateOneRecord.test.tsx | 15 +- .../utils/computeDraftValueFromString.ts | 14 + .../hooks/__tests__/useTableData.test.tsx | 26 +- .../components/__stories__/perf/mock.ts | 5 +- .../display/components/PhonesDisplay.tsx | 30 +- .../src/testing/mock-data/people.ts | 80 ++++- .../commands/database-command.module.ts | 2 + ...-migrate-phone-fields-to-phones.command.ts | 338 ++++++++++++++++++ .../0-32/0-32-upgrade-version.command.ts | 46 +++ .../0-32/0-32-upgrade-version.module.ts | 32 ++ .../typeorm-seeds/metadata/fieldsMetadata.ts | 2 +- .../typeorm-seeds/workspace/people.ts | 96 +++-- .../standard-objects-prefill-data/company.ts | 12 + .../standard-objects-prefill-data/person.ts | 26 ++ .../views/people-all.view.ts | 2 +- .../constants/standard-field-ids.ts | 1 + .../person.workspace-entity.ts | 11 + .../test/people.integration-spec.ts | 12 +- 25 files changed, 709 insertions(+), 78 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-migrate-phone-fields-to-phones.command.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.command.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.module.ts 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 da578f8973f8..215cb8af8630 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 5a00909648f2..4ab9bebb9ba2 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 2b2fd70bb53d..7fd6050ad380 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 2f0fdfcee60b..6e109248be53 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 967f1ac6a5d9..fd28307168b5 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 cb22a045d519..6804a92c3525 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 4cc34f4932eb..44fe83664141 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 0ccd92897a89..eb6e7048d316 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 d41d5fd85100..c668801e33aa 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 bac46e65238e..2cacb3e8cdf6 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 52c11a2044d9..ce0253ddab59 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 17e9d27f4d29..deee867fc16e 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 439730e43ae8..9139aa194367 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 9d98d33abd75..e390ab16277e 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 000000000000..d5c1003335c3 --- /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 000000000000..dd1a6db03207 --- /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 000000000000..cabf5ef86d27 --- /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 ec81351541be..330975f00d02 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 6624fd50331e..22adfe0142e9 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 8d5a24a1eef8..2f3966e1043e 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 fb227b15ca56..ec07c61f15b2 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 6beb5cda8c4d..9fbddef9109b 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 8f0d135fdbb3..8a19d49e4b2c 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 a39a401ca02d..e162bb82b7b9 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 b96bb670085c..fd568bd5ba40 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');