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 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');