Support for multiple values in the Phone field (#6882)

### Description

- This is the first PR on Phones field;


- We are introducing new field type(Phones)


- We are Forbidding creation of Phone field


- We Added support for filtering and sorting on Phones field


- We are using the same display mode as used on the Links field type
(chips), check the Domain field of the Company object


- We are also using the same logic of the link when editing the field

**How to Test**

1. Checkout to TWNTY-6260 branch
2. Reset database using "npx nx database:reset twenty-server" command
3. Add custom field of type Phones in settings/data-model

**Loom Video:**\

<https://www.loom.com/share/3c981260be254dcf851256d020a20ab0?sid=58507361-3a3b-452c-9de8-b5b1abda70ac>

### Refs

#6260

Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com>
This commit is contained in:
gitstart-app[bot]
2024-09-11 11:15:04 +02:00
committed by GitHub
parent 91187dcf82
commit 846953b0f4
52 changed files with 793 additions and 64 deletions

View File

@ -7,6 +7,7 @@ export const FIELD_CURRENCY_MOCK_NAME = 'fieldCurrency';
export const FIELD_ADDRESS_MOCK_NAME = 'fieldAddress';
export const FIELD_ACTOR_MOCK_NAME = 'fieldActor';
export const FIELD_FULL_NAME_MOCK_NAME = 'fieldFullName';
export const FIELD_PHONES_MOCK_NAME = 'fieldPhones';
export const fieldNumberMock = {
name: 'fieldNumber',
@ -221,6 +222,7 @@ const fieldActorMock = {
name: '',
},
};
const fieldEmailsMock = {
name: 'fieldEmails',
type: FieldMetadataType.EMAILS,
@ -228,10 +230,24 @@ const fieldEmailsMock = {
defaultValue: [{ primaryEmail: '', additionalEmails: {} }],
};
const fieldPhonesMock = {
name: FIELD_PHONES_MOCK_NAME,
type: FieldMetadataType.PHONES,
isNullable: false,
defaultValue: [
{
primaryPhoneNumber: '',
primaryPhoneCountryCode: '',
additionalPhones: {},
},
],
};
export const fields = [
fieldUuidMock,
fieldTextMock,
fieldPhoneMock,
fieldPhonesMock,
fieldEmailMock,
fieldEmailsMock,
fieldDateTimeMock,

View File

@ -152,5 +152,14 @@ export const mapFieldMetadataToGraphqlQuery = (
additionalEmails
}
`;
} else if (fieldType === FieldMetadataType.PHONES) {
return `
${field.name}
{
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
`;
}
};

View File

@ -33,6 +33,20 @@ describe('computeSchemaComponents', () => {
fieldPhone: {
type: 'string',
},
fieldPhones: {
properties: {
additionalPhones: {
type: 'object',
},
primaryPhoneCountryCode: {
type: 'string',
},
primaryPhoneNumber: {
type: 'string',
},
},
type: 'object',
},
fieldEmail: {
type: 'string',
format: 'email',
@ -195,6 +209,20 @@ describe('computeSchemaComponents', () => {
fieldPhone: {
type: 'string',
},
fieldPhones: {
properties: {
additionalPhones: {
type: 'object',
},
primaryPhoneCountryCode: {
type: 'string',
},
primaryPhoneNumber: {
type: 'string',
},
},
type: 'object',
},
fieldEmail: {
type: 'string',
format: 'email',
@ -356,6 +384,20 @@ describe('computeSchemaComponents', () => {
fieldPhone: {
type: 'string',
},
fieldPhones: {
properties: {
additionalPhones: {
type: 'object',
},
primaryPhoneCountryCode: {
type: 'string',
},
primaryPhoneNumber: {
type: 'string',
},
},
type: 'object',
},
fieldEmail: {
type: 'string',
format: 'email',

View File

@ -137,6 +137,7 @@ const getSchemaComponentsProperties = ({
case FieldMetadataType.ADDRESS:
case FieldMetadataType.ACTOR:
case FieldMetadataType.EMAILS:
case FieldMetadataType.PHONES:
itemProperty = {
type: 'object',
properties: compositeTypeDefinitions

View File

@ -7,6 +7,7 @@ import { emailsCompositeType } from 'src/engine/metadata-modules/field-metadata/
import { fullNameCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
import { linkCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/link.composite-type';
import { linksCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
import { phonesCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export const compositeTypeDefinitions = new Map<
@ -20,4 +21,5 @@ export const compositeTypeDefinitions = new Map<
[FieldMetadataType.ADDRESS, addressCompositeType],
[FieldMetadataType.ACTOR, actorCompositeType],
[FieldMetadataType.EMAILS, emailsCompositeType],
[FieldMetadataType.PHONES, phonesCompositeType],
]);

View File

@ -0,0 +1,33 @@
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export const phonesCompositeType: CompositeType = {
type: FieldMetadataType.PHONES,
properties: [
{
name: 'primaryPhoneNumber',
type: FieldMetadataType.TEXT,
hidden: false,
isRequired: false,
},
{
name: 'primaryPhoneCountryCode',
type: FieldMetadataType.TEXT,
hidden: false,
isRequired: false,
},
{
name: 'additionalPhones',
type: FieldMetadataType.RAW_JSON,
hidden: false,
isRequired: false,
},
],
};
export type PhonesMetadata = {
primaryPhoneNumber: string;
primaryPhoneCountryCode: string;
additionalPhones: object | null;
};

View File

@ -185,3 +185,17 @@ export class FieldMetadataDefaultValueEmails {
@IsObject()
additionalEmails: string[] | null;
}
export class FieldMetadataDefaultValuePhones {
@ValidateIf((_object, value) => value !== null)
@IsQuotedString()
primaryPhoneNumber: string | null;
@ValidateIf((_object, value) => value !== null)
@IsQuotedString()
primaryPhoneCountryCode: string | null;
@ValidateIf((_object, value) => value !== null)
@IsObject()
additionalPhones: object | null;
}

View File

@ -25,6 +25,7 @@ export enum FieldMetadataType {
UUID = 'UUID',
TEXT = 'TEXT',
PHONE = 'PHONE',
PHONES = 'PHONES',
EMAIL = 'EMAIL',
EMAILS = 'EMAILS',
DATE_TIME = 'DATE_TIME',

View File

@ -10,6 +10,7 @@ import {
FieldMetadataDefaultValueLinks,
FieldMetadataDefaultValueNowFunction,
FieldMetadataDefaultValueNumber,
FieldMetadataDefaultValuePhones,
FieldMetadataDefaultValueRawJson,
FieldMetadataDefaultValueRichText,
FieldMetadataDefaultValueString,
@ -27,6 +28,7 @@ type FieldMetadataDefaultValueMapping = {
| FieldMetadataDefaultValueUuidFunction;
[FieldMetadataType.TEXT]: FieldMetadataDefaultValueString;
[FieldMetadataType.PHONE]: FieldMetadataDefaultValueString;
[FieldMetadataType.PHONES]: FieldMetadataDefaultValuePhones;
[FieldMetadataType.EMAIL]: FieldMetadataDefaultValueString;
[FieldMetadataType.EMAILS]: FieldMetadataDefaultValueEmails;
[FieldMetadataType.DATE_TIME]:

View File

@ -47,6 +47,12 @@ export function generateDefaultValue(
primaryLinkUrl: "''",
secondaryLinks: null,
};
case FieldMetadataType.PHONES:
return {
primaryPhoneNumber: "''",
primaryPhoneCountryCode: "''",
additionalPhones: null,
};
default:
return null;
}

View File

@ -9,7 +9,8 @@ export const isCompositeFieldMetadataType = (
| FieldMetadataType.ADDRESS
| FieldMetadataType.LINKS
| FieldMetadataType.ACTOR
| FieldMetadataType.EMAILS => {
| FieldMetadataType.EMAILS
| FieldMetadataType.PHONES => {
return [
FieldMetadataType.LINK,
FieldMetadataType.CURRENCY,
@ -18,5 +19,6 @@ export const isCompositeFieldMetadataType = (
FieldMetadataType.LINKS,
FieldMetadataType.ACTOR,
FieldMetadataType.EMAILS,
FieldMetadataType.PHONES,
].includes(type);
};

View File

@ -19,6 +19,7 @@ import {
FieldMetadataDefaultValueLinks,
FieldMetadataDefaultValueNowFunction,
FieldMetadataDefaultValueNumber,
FieldMetadataDefaultValuePhones,
FieldMetadataDefaultValueRawJson,
FieldMetadataDefaultValueString,
FieldMetadataDefaultValueStringArray,
@ -55,6 +56,7 @@ export const defaultValueValidatorsMap = {
[FieldMetadataType.LINKS]: [FieldMetadataDefaultValueLinks],
[FieldMetadataType.ACTOR]: [FieldMetadataDefaultActor],
[FieldMetadataType.EMAILS]: [FieldMetadataDefaultValueEmails],
[FieldMetadataType.PHONES]: [FieldMetadataDefaultValuePhones],
};
type ValidationResult = {

View File

@ -24,7 +24,8 @@ export type CompositeFieldMetadataType =
| FieldMetadataType.FULL_NAME
| FieldMetadataType.LINK
| FieldMetadataType.LINKS
| FieldMetadataType.EMAILS;
| FieldMetadataType.EMAILS
| FieldMetadataType.PHONES;
@Injectable()
export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<CompositeFieldMetadataType> {

View File

@ -101,6 +101,10 @@ export class WorkspaceMigrationFactory {
FieldMetadataType.EMAILS,
{ factory: this.compositeColumnActionFactory },
],
[
FieldMetadataType.PHONES,
{ factory: this.compositeColumnActionFactory },
],
]);
}