Changed the auto matching of columns in import (#12181)

This PR changes the way we do automatching in the import feature.

It uses [Fuse.js](https://www.fusejs.io/) to do a fuzzy text search on
fields and sub-fields.

The labels of sub-fields are now derived from the common config constant
we have for sub-fields.
This commit is contained in:
Lucas Bordeau
2025-05-23 18:33:18 +02:00
committed by GitHub
parent f7ccb5d207
commit 371fdba1f8
9 changed files with 121 additions and 46 deletions

View File

@ -9,43 +9,80 @@ import {
FieldRichTextV2Value,
} from '@/object-record/record-field/types/FieldMetadata';
import { CompositeFieldLabels } from '@/object-record/spreadsheet-import/types/CompositeFieldLabels';
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const COMPOSITE_FIELD_IMPORT_LABELS = {
[FieldMetadataType.FULL_NAME]: {
firstNameLabel: 'First Name',
lastNameLabel: 'Last Name',
firstNameLabel:
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.FULL_NAME.labelBySubField.firstName,
lastNameLabel:
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.FULL_NAME.labelBySubField.lastName,
} satisfies CompositeFieldLabels<FieldFullNameValue>,
[FieldMetadataType.CURRENCY]: {
currencyCodeLabel: 'Currency Code',
amountMicrosLabel: 'Amount',
currencyCodeLabel:
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.CURRENCY.labelBySubField
.currencyCode,
amountMicrosLabel:
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.CURRENCY.labelBySubField
.amountMicros,
} satisfies CompositeFieldLabels<FieldCurrencyValue>,
[FieldMetadataType.ADDRESS]: {
addressStreet1Label: 'Address 1',
addressStreet2Label: 'Address 2',
addressCityLabel: 'City',
addressPostcodeLabel: 'Post Code',
addressStateLabel: 'State',
addressCountryLabel: 'Country',
addressLatLabel: 'Latitude',
addressLngLabel: 'Longitude',
} satisfies CompositeFieldLabels<FieldAddressValue>,
addressStreet1Label:
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ADDRESS.labelBySubField
.addressStreet1,
addressStreet2Label:
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ADDRESS.labelBySubField
.addressStreet2,
addressCityLabel:
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ADDRESS.labelBySubField.addressCity,
addressPostcodeLabel:
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ADDRESS.labelBySubField
.addressPostcode,
addressStateLabel:
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ADDRESS.labelBySubField
.addressState,
addressCountryLabel:
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ADDRESS.labelBySubField
.addressCountry,
} satisfies Omit<
CompositeFieldLabels<FieldAddressValue>,
'addressLatLabel' | 'addressLngLabel'
>,
[FieldMetadataType.LINKS]: {
// primaryLinkLabelLabel excluded from composite field import labels since it's not used in Links input
primaryLinkUrlLabel: 'Link URL',
primaryLinkUrlLabel:
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.LINKS.labelBySubField
.primaryLinkUrl,
secondaryLinksLabel:
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.LINKS.labelBySubField
.secondaryLinks,
} satisfies Partial<CompositeFieldLabels<FieldLinksValue>>,
[FieldMetadataType.EMAILS]: {
primaryEmailLabel: 'Email',
primaryEmailLabel:
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.EMAILS.labelBySubField.primaryEmail,
additionalEmailsLabel:
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.EMAILS.labelBySubField
.additionalEmails,
} satisfies Partial<CompositeFieldLabels<FieldEmailsValue>>,
[FieldMetadataType.PHONES]: {
primaryPhoneCountryCodeLabel: 'Phone country code',
primaryPhoneNumberLabel: 'Phone number',
primaryPhoneCountryCodeLabel:
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.PHONES.labelBySubField
.primaryPhoneCountryCode,
primaryPhoneNumberLabel:
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.PHONES.labelBySubField
.primaryPhoneNumber,
} satisfies Partial<CompositeFieldLabels<FieldPhonesValue>>,
[FieldMetadataType.RICH_TEXT_V2]: {
blocknoteLabel: 'BlockNote',
markdownLabel: 'Markdown',
blocknoteLabel:
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.RICH_TEXT_V2.labelBySubField
.blocknote,
markdownLabel:
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.RICH_TEXT_V2.labelBySubField
.markdown,
} satisfies Partial<CompositeFieldLabels<FieldRichTextV2Value>>,
[FieldMetadataType.ACTOR]: {
sourceLabel: 'Source',
sourceLabel:
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ACTOR.labelBySubField.source,
nameLabel: SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ACTOR.labelBySubField.name,
} satisfies Partial<CompositeFieldLabels<FieldActorValue>>,
};

View File

@ -30,8 +30,6 @@ export const buildRecordFromImportedStructuredRow = ({
ADDRESS: {
addressCityLabel,
addressCountryLabel,
addressLatLabel,
addressLngLabel,
addressPostcodeLabel,
addressStateLabel,
addressStreet1Label,
@ -88,9 +86,7 @@ export const buildRecordFromImportedStructuredRow = ({
`${addressPostcodeLabel} (${field.name})`
] ||
importedStructuredRow[`${addressStateLabel} (${field.name})`] ||
importedStructuredRow[`${addressCountryLabel} (${field.name})`] ||
importedStructuredRow[`${addressLatLabel} (${field.name})`] ||
importedStructuredRow[`${addressLngLabel} (${field.name})`],
importedStructuredRow[`${addressCountryLabel} (${field.name})`],
)
) {
recordToBuild[field.name] = {
@ -112,13 +108,7 @@ export const buildRecordFromImportedStructuredRow = ({
addressCountry: castToString(
importedStructuredRow[`${addressCountryLabel} (${field.name})`],
),
addressLat: Number(
importedStructuredRow[`${addressLatLabel} (${field.name})`],
),
addressLng: Number(
importedStructuredRow[`${addressLngLabel} (${field.name})`],
),
} satisfies FieldAddressValue;
} satisfies Partial<FieldAddressValue>;
}
break;
}

View File

@ -1,3 +1,4 @@
import { FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata';
import { SpreadsheetImportFieldValidationDefinition } from '@/spreadsheet-import/types';
import { absoluteUrlSchema, isDefined, isValidUuid } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -50,9 +51,11 @@ export const getSpreadSheetFieldValidationDefinitions = (
case FieldMetadataType.LINKS:
return [
{
rule: 'function',
isValid: (value: string) =>
absoluteUrlSchema.safeParse(value).success,
rule: 'object',
isValid: ({
primaryLinkUrl,
}: Pick<FieldLinksValue, 'primaryLinkUrl' | 'secondaryLinks'>) =>
absoluteUrlSchema.safeParse(primaryLinkUrl).success,
errorMessage: fieldName + ' is not valid',
level: 'error',
},