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:**\

<https://www.loom.com/share/4b9bcb423cee447d8ad09852a83b27da?sid=ed74ecaa-0339-4575-acdc-a863e95e94fd>

### Refs

#6260

---------

Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com>
Co-authored-by: Marie Stoppa <marie.stoppa@essec.edu>
Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
gitstart-app[bot]
2024-09-24 16:31:30 +02:00
committed by GitHub
parent b83f0f46e5
commit fa241fa4e9
25 changed files with 709 additions and 78 deletions

View File

@ -197,6 +197,10 @@ name
lastName
}
phone
{
primaryPhoneNumber
primaryPhoneCountryCode
}
linkedinLink
{
primaryLinkUrl

View File

@ -46,7 +46,11 @@ describe('mapObjectMetadataToGraphQLQuery', () => {
primaryEmail
additionalEmails
}
phone
phone
{
primaryPhoneNumber
primaryPhoneCountryCode
}
createdAt
avatarUrl
jobTitle

View File

@ -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": "Contacts phone number",

View File

@ -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

View File

@ -36,7 +36,10 @@ export const responseData = {
firstName: '',
lastName: '',
},
phone: '',
phones: {
primaryPhoneCountryCode: '',
primaryPhoneNumber: '',
},
linkedinLink: {
primaryLinkUrl: '',
primaryLinkLabel: '',

View File

@ -41,7 +41,10 @@ export const responseData = {
firstName: '',
lastName: '',
},
phone: '',
phones: {
primaryPhoneCountryCode: '',
primaryPhoneNumber: '',
},
linkedinLink: {
primaryLinkUrl: '',
primaryLinkLabel: '',

View File

@ -24,7 +24,6 @@ const basePerson = {
firstName: '',
lastName: '',
},
phone: '',
linkedinLink: {
primaryLinkUrl: '',
primaryLinkLabel: '',

View File

@ -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 = [
{

View File

@ -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 = <FieldValue>({
} as FieldInputDraftValue<FieldValue>;
}
if (isFieldEmails(fieldDefinition)) {
return {
primaryEmail: value,
} as FieldInputDraftValue<FieldValue>;
}
if (isFieldPhones(fieldDefinition)) {
return {
primaryPhoneNumber: value,
} as FieldInputDraftValue<FieldValue>;
}
throw new Error(`Record field type not supported : ${fieldDefinition.type}}`);
};

View File

@ -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

View File

@ -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: '',

View File

@ -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 ? (
<ExpandableList isChipCountDisplayed>
{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 (
<RoundedLink
key={index}
href={URI}
label={parsedPhone.formatInternational()}
href={URI || ''}
label={
parsedPhone ? parsedPhone.formatInternational() : invalidPhone
}
/>
);
})}
@ -73,13 +84,16 @@ export const PhonesDisplay = ({ value, isFocused }: PhonesDisplayProps) => {
) : (
<StyledContainer>
{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 (
<RoundedLink
key={index}
href={URI}
label={parsedPhone.formatInternational()}
href={URI || ''}
label={
parsedPhone ? parsedPhone.formatInternational() : invalidPhone
}
/>
);
})}