From eb7d120321a310c9fa3b9b0a0df339812327f73c Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Sat, 28 Sep 2024 16:11:10 +0200 Subject: [PATCH] Support Emails and Phones in Spreadsheet import (#7312) This is a fast follow on v0.30 release: - removing phone (deprecated PHONE field type) search from command menu. I could have replaced it by a phone (PHONES field type) search but as we are about to release the new search in v0.31 it does not seem to worse the investment - supporting EMAILS and PHONES field types in spreadsheet import Note: while working on Spreadsheet import I found the code quite complex and with areas having duplicated code. It does not seem to be a high priority as I was able to maintain it at a low cost but it's not a peaceful code surface to navigate! --- .../command-menu/components/CommandMenu.tsx | 1 - .../constants/CompositeFieldImportLabels.ts | 9 ++++ .../hooks/useBuildAvailableFieldsForImport.ts | 37 ++++++++++++++ .../buildRecordFromImportedStructuredRow.ts | 48 +++++++++++++++++-- .../utils/sanitizeRecordInput.ts | 2 + 5 files changed, 91 insertions(+), 6 deletions(-) diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx index 2b7d13f8c52b..1c238462c55f 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx @@ -179,7 +179,6 @@ export const CommandMenu = () => { 'emails', ['primaryEmail'], ), - { phone: { ilike: `%${commandMenuSearch}%` } }, ]) : undefined, limit: 3, diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts index 35a697bc116d..030601f241bc 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts @@ -1,8 +1,10 @@ import { FieldAddressValue, FieldCurrencyValue, + FieldEmailsValue, FieldFullNameValue, FieldLinksValue, + FieldPhonesValue, } from '@/object-record/record-field/types/FieldMetadata'; import { CompositeFieldLabels } from '@/object-record/spreadsheet-import/types/CompositeFieldLabels'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -30,6 +32,13 @@ export const COMPOSITE_FIELD_IMPORT_LABELS = { primaryLinkUrlLabel: 'Link URL', primaryLinkLabelLabel: 'Link Label', } satisfies Partial>, + [FieldMetadataType.Emails]: { + primaryEmailLabel: 'Email', + } satisfies Partial>, + [FieldMetadataType.Phones]: { + primaryPhoneCountryCodeLabel: 'Phone country code', + primaryPhoneNumberLabel: 'Phone number', + } satisfies Partial>, [FieldMetadataType.Actor]: { sourceLabel: 'Source', }, diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts index 7e4edf83926a..260a223f1791 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts @@ -15,6 +15,7 @@ export const useBuildAvailableFieldsForImport = () => { ) => { const availableFieldsForImport: AvailableFieldForImport[] = []; + // Todo: refactor this to avoid this else if syntax with duplicated code for (const fieldMetadataItem of fieldMetadataItems) { if (fieldMetadataItem.type === FieldMetadataType.FullName) { const { firstNameLabel, lastNameLabel } = @@ -155,6 +156,42 @@ export const useBuildAvailableFieldsForImport = () => { fieldMetadataItem.label, ), }); + } else if (fieldMetadataItem.type === FieldMetadataType.Emails) { + Object.entries( + COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.Emails], + ).forEach(([_, fieldLabel]) => { + availableFieldsForImport.push({ + icon: getIcon(fieldMetadataItem.icon), + label: `${fieldLabel} (${fieldMetadataItem.label})`, + key: `${fieldLabel} (${fieldMetadataItem.name})`, + fieldType: { + type: 'input', + }, + fieldValidationDefinitions: + getSpreadSheetFieldValidationDefinitions( + fieldMetadataItem.type, + `${fieldLabel} (${fieldMetadataItem.label})`, + ), + }); + }); + } else if (fieldMetadataItem.type === FieldMetadataType.Phones) { + Object.entries( + COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.Phones], + ).forEach(([_, fieldLabel]) => { + availableFieldsForImport.push({ + icon: getIcon(fieldMetadataItem.icon), + label: `${fieldLabel} (${fieldMetadataItem.label})`, + key: `${fieldLabel} (${fieldMetadataItem.name})`, + fieldType: { + type: 'input', + }, + fieldValidationDefinitions: + getSpreadSheetFieldValidationDefinitions( + fieldMetadataItem.type, + `${fieldLabel} (${fieldMetadataItem.label})`, + ), + }); + }); } else { availableFieldsForImport.push({ icon: getIcon(fieldMetadataItem.icon), diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/util/buildRecordFromImportedStructuredRow.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/util/buildRecordFromImportedStructuredRow.ts index 5ddbe06096b5..68e641656660 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/util/buildRecordFromImportedStructuredRow.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/util/buildRecordFromImportedStructuredRow.ts @@ -1,7 +1,9 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldAddressValue, + FieldEmailsValue, FieldLinksValue, + FieldPhonesValue, } from '@/object-record/record-field/types/FieldMetadata'; import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels'; import { ImportedStructuredRow } from '@/spreadsheet-import/types'; @@ -31,6 +33,8 @@ export const buildRecordFromImportedStructuredRow = ( CURRENCY: { amountMicrosLabel, currencyCodeLabel }, FULL_NAME: { firstNameLabel, lastNameLabel }, LINKS: { primaryLinkLabelLabel, primaryLinkUrlLabel }, + EMAILS: { primaryEmailLabel }, + PHONES: { primaryPhoneNumberLabel, primaryPhoneCountryCodeLabel }, } = COMPOSITE_FIELD_IMPORT_LABELS; for (const field of fields) { @@ -129,14 +133,48 @@ export const buildRecordFromImportedStructuredRow = ( } break; } - case FieldMetadataType.Link: - if (importedFieldValue !== undefined) { + case FieldMetadataType.Phones: { + if ( + isDefined( + importedStructuredRow[ + `${primaryPhoneCountryCodeLabel} (${field.name})` + ] || + importedStructuredRow[ + `${primaryPhoneNumberLabel} (${field.name})` + ], + ) + ) { recordToBuild[field.name] = { - label: field.name, - url: importedFieldValue || null, - }; + primaryPhoneCountryCode: castToString( + importedStructuredRow[ + `${primaryPhoneCountryCodeLabel} (${field.name})` + ], + ), + primaryPhoneNumber: castToString( + importedStructuredRow[ + `${primaryPhoneNumberLabel} (${field.name})` + ], + ), + additionalPhones: null, + } satisfies FieldPhonesValue; } break; + } + case FieldMetadataType.Emails: { + if ( + isDefined( + importedStructuredRow[`${primaryEmailLabel} (${field.name})`], + ) + ) { + recordToBuild[field.name] = { + primaryEmail: castToString( + importedStructuredRow[`${primaryEmailLabel} (${field.name})`], + ), + additionalEmails: null, + } satisfies FieldEmailsValue; + } + break; + } case FieldMetadataType.Relation: if ( isDefined(importedFieldValue) && diff --git a/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts b/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts index cc3fb4ba46fa..c760351487a7 100644 --- a/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts +++ b/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts @@ -50,6 +50,8 @@ export const sanitizeRecordInput = ({ return undefined; } + // Todo: we should check that the fieldValue is a valid value + // (e.g. a string for a string field, following the right composite structure for composite fields) return [fieldName, fieldValue]; }) .filter(isDefined),