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!
This commit is contained in:
Charles Bochet 2024-09-28 16:11:10 +02:00 committed by GitHub
parent e4959ad534
commit c2a8cd0a2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 91 additions and 6 deletions

View File

@ -179,7 +179,6 @@ export const CommandMenu = () => {
'emails', 'emails',
['primaryEmail'], ['primaryEmail'],
), ),
{ phone: { ilike: `%${commandMenuSearch}%` } },
]) ])
: undefined, : undefined,
limit: 3, limit: 3,

View File

@ -1,8 +1,10 @@
import { import {
FieldAddressValue, FieldAddressValue,
FieldCurrencyValue, FieldCurrencyValue,
FieldEmailsValue,
FieldFullNameValue, FieldFullNameValue,
FieldLinksValue, FieldLinksValue,
FieldPhonesValue,
} from '@/object-record/record-field/types/FieldMetadata'; } from '@/object-record/record-field/types/FieldMetadata';
import { CompositeFieldLabels } from '@/object-record/spreadsheet-import/types/CompositeFieldLabels'; import { CompositeFieldLabels } from '@/object-record/spreadsheet-import/types/CompositeFieldLabels';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -30,6 +32,13 @@ export const COMPOSITE_FIELD_IMPORT_LABELS = {
primaryLinkUrlLabel: 'Link URL', primaryLinkUrlLabel: 'Link URL',
primaryLinkLabelLabel: 'Link Label', primaryLinkLabelLabel: 'Link Label',
} satisfies Partial<CompositeFieldLabels<FieldLinksValue>>, } satisfies Partial<CompositeFieldLabels<FieldLinksValue>>,
[FieldMetadataType.Emails]: {
primaryEmailLabel: 'Email',
} satisfies Partial<CompositeFieldLabels<FieldEmailsValue>>,
[FieldMetadataType.Phones]: {
primaryPhoneCountryCodeLabel: 'Phone country code',
primaryPhoneNumberLabel: 'Phone number',
} satisfies Partial<CompositeFieldLabels<FieldPhonesValue>>,
[FieldMetadataType.Actor]: { [FieldMetadataType.Actor]: {
sourceLabel: 'Source', sourceLabel: 'Source',
}, },

View File

@ -15,6 +15,7 @@ export const useBuildAvailableFieldsForImport = () => {
) => { ) => {
const availableFieldsForImport: AvailableFieldForImport[] = []; const availableFieldsForImport: AvailableFieldForImport[] = [];
// Todo: refactor this to avoid this else if syntax with duplicated code
for (const fieldMetadataItem of fieldMetadataItems) { for (const fieldMetadataItem of fieldMetadataItems) {
if (fieldMetadataItem.type === FieldMetadataType.FullName) { if (fieldMetadataItem.type === FieldMetadataType.FullName) {
const { firstNameLabel, lastNameLabel } = const { firstNameLabel, lastNameLabel } =
@ -155,6 +156,42 @@ export const useBuildAvailableFieldsForImport = () => {
fieldMetadataItem.label, 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 { } else {
availableFieldsForImport.push({ availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon), icon: getIcon(fieldMetadataItem.icon),

View File

@ -1,7 +1,9 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { import {
FieldAddressValue, FieldAddressValue,
FieldEmailsValue,
FieldLinksValue, FieldLinksValue,
FieldPhonesValue,
} from '@/object-record/record-field/types/FieldMetadata'; } from '@/object-record/record-field/types/FieldMetadata';
import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels'; import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels';
import { ImportedStructuredRow } from '@/spreadsheet-import/types'; import { ImportedStructuredRow } from '@/spreadsheet-import/types';
@ -31,6 +33,8 @@ export const buildRecordFromImportedStructuredRow = (
CURRENCY: { amountMicrosLabel, currencyCodeLabel }, CURRENCY: { amountMicrosLabel, currencyCodeLabel },
FULL_NAME: { firstNameLabel, lastNameLabel }, FULL_NAME: { firstNameLabel, lastNameLabel },
LINKS: { primaryLinkLabelLabel, primaryLinkUrlLabel }, LINKS: { primaryLinkLabelLabel, primaryLinkUrlLabel },
EMAILS: { primaryEmailLabel },
PHONES: { primaryPhoneNumberLabel, primaryPhoneCountryCodeLabel },
} = COMPOSITE_FIELD_IMPORT_LABELS; } = COMPOSITE_FIELD_IMPORT_LABELS;
for (const field of fields) { for (const field of fields) {
@ -129,14 +133,48 @@ export const buildRecordFromImportedStructuredRow = (
} }
break; break;
} }
case FieldMetadataType.Link: case FieldMetadataType.Phones: {
if (importedFieldValue !== undefined) { if (
isDefined(
importedStructuredRow[
`${primaryPhoneCountryCodeLabel} (${field.name})`
] ||
importedStructuredRow[
`${primaryPhoneNumberLabel} (${field.name})`
],
)
) {
recordToBuild[field.name] = { recordToBuild[field.name] = {
label: field.name, primaryPhoneCountryCode: castToString(
url: importedFieldValue || null, importedStructuredRow[
}; `${primaryPhoneCountryCodeLabel} (${field.name})`
],
),
primaryPhoneNumber: castToString(
importedStructuredRow[
`${primaryPhoneNumberLabel} (${field.name})`
],
),
additionalPhones: null,
} satisfies FieldPhonesValue;
} }
break; 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: case FieldMetadataType.Relation:
if ( if (
isDefined(importedFieldValue) && isDefined(importedFieldValue) &&

View File

@ -50,6 +50,8 @@ export const sanitizeRecordInput = ({
return undefined; 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]; return [fieldName, fieldValue];
}) })
.filter(isDefined), .filter(isDefined),