From 950ce4434188d052fffee82e8f46c6b38dd9eb56 Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Wed, 24 Sep 2025 18:38:42 +0200 Subject: [PATCH] Import - Fix erase on update on Number, Boolean and Multi Select fields (#14686) Before on an object with Number, Boolean, Multi-Select fields --> createMany input from import feature with only one name firstName column matched : ``` { "data": [ { "booleanField": false, "createdBy": { "source": "IMPORT", "context": {} }, "name": { "firstName": "Test" }, "multiSelectField": [], "numberField": null }, { "booleanField": false, "createdBy": { "source": "IMPORT", "context": {} }, "name": { "firstName": "Test1" }, "multiSelectField": [], "numberField": null } ], "upsert": true } ``` After with same object : ``` { "data": [ { "createdBy": { "source": "IMPORT", "context": {} }, "name": { "firstName": "Test" } }, { "createdBy": { "source": "IMPORT", "context": {} }, "name": { "firstName": "Test1" } } ], "upsert": true } ``` Fixes https://github.com/twentyhq/twenty/issues/14672 --- ...ildRecordFromImportedStructuredRow.test.ts | 679 +++++++++--------- .../buildRecordFromImportedStructuredRow.ts | 22 +- 2 files changed, 356 insertions(+), 345 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/__tests__/buildRecordFromImportedStructuredRow.test.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/__tests__/buildRecordFromImportedStructuredRow.test.ts index e82050502a..5710c32803 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/__tests__/buildRecordFromImportedStructuredRow.test.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/__tests__/buildRecordFromImportedStructuredRow.test.ts @@ -9,6 +9,302 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; import { RelationType } from '~/generated/graphql'; describe('buildRecordFromImportedStructuredRow', () => { + const fields: FieldMetadataItem[] = [ + { + id: '3', + name: 'booleanField', + label: 'Boolean Field', + type: FieldMetadataType.BOOLEAN, + isNullable: true, + isActive: true, + isCustom: false, + isSystem: false, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + icon: 'IconCheck', + description: null, + }, + { + id: '4', + name: 'numberField', + label: 'Number Field', + type: FieldMetadataType.NUMBER, + isNullable: true, + isActive: true, + isCustom: false, + isSystem: false, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + icon: 'IconNumber', + description: null, + }, + { + id: '5', + name: 'multiSelectField', + label: 'Multi-Select Field', + type: FieldMetadataType.MULTI_SELECT, + isNullable: true, + isActive: true, + isCustom: false, + isSystem: false, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + icon: 'IconTag', + description: null, + options: [ + { + id: '1', + value: 'tag1', + label: 'Tag 1', + color: 'blue', + position: 0, + }, + { + id: '2', + value: 'tag2', + label: 'Tag 2', + color: 'red', + position: 1, + }, + { + id: '3', + value: 'tag3', + label: 'Tag 3', + color: 'green', + position: 2, + }, + ], + }, + { + id: '6', + name: 'relationField', + label: 'Relation Field', + type: FieldMetadataType.RELATION, + isNullable: true, + isActive: true, + isCustom: false, + isSystem: false, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + icon: 'IconBuilding', + description: null, + relation: { + type: RelationType.MANY_TO_ONE, + } as FieldMetadataItemRelation, + }, + { + id: '7', + name: 'fullNameField', + label: 'Full Name Field', + type: FieldMetadataType.FULL_NAME, + isNullable: true, + isActive: true, + isCustom: false, + isSystem: false, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + icon: 'IconUser', + description: null, + }, + { + id: '8', + name: 'currencyField', + label: 'Currency Field', + type: FieldMetadataType.CURRENCY, + isNullable: true, + isActive: true, + isCustom: false, + isSystem: false, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + icon: 'IconCurrencyDollar', + description: null, + }, + { + id: '9', + name: 'addressField', + label: 'Address Field', + type: FieldMetadataType.ADDRESS, + isNullable: true, + isActive: true, + isCustom: false, + isSystem: false, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + icon: 'IconMap', + description: null, + }, + { + id: '10', + name: 'selectField', + label: 'Select Field', + type: FieldMetadataType.SELECT, + isNullable: true, + isActive: true, + isCustom: false, + isSystem: false, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + icon: 'IconTag', + description: null, + options: [ + { + id: '1', + value: 'option1', + label: 'Option 1', + color: 'blue', + position: 0, + }, + { + id: '2', + value: 'option2', + label: 'Option 2', + color: 'red', + position: 1, + }, + ], + }, + { + id: '11', + name: 'arrayField', + label: 'Array Field', + type: FieldMetadataType.ARRAY, + isNullable: true, + isActive: true, + isCustom: false, + isSystem: false, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + icon: 'IconBracketsContain', + description: null, + }, + { + id: '12', + name: 'jsonField', + label: 'JSON Field', + type: FieldMetadataType.RAW_JSON, + isNullable: true, + isActive: true, + isCustom: false, + isSystem: false, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + icon: 'IconBraces', + description: null, + }, + { + id: '13', + name: 'phoneField', + label: 'Phone Field', + type: FieldMetadataType.PHONES, + isNullable: true, + isActive: true, + isCustom: false, + isSystem: false, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + icon: 'IconPhone', + description: null, + }, + { + id: '14', + name: 'linksField', + label: 'Links Field', + type: FieldMetadataType.LINKS, + isNullable: true, + isActive: true, + isCustom: false, + isSystem: false, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + icon: 'IconWorld', + description: null, + }, + { + id: '15', + name: 'createdBy', + label: 'Created by', + type: FieldMetadataType.ACTOR, + isNullable: true, + isActive: true, + isCustom: false, + isSystem: false, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + icon: 'IconUsers', + description: null, + }, + { + id: '16', + name: 'richTextField', + label: 'Rich Text Field', + type: FieldMetadataType.RICH_TEXT_V2, + isNullable: true, + isActive: true, + isCustom: false, + isSystem: false, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + icon: 'IconTextEditor', + description: null, + }, + { + id: '17', + name: 'dateField', + label: 'Date Field', + type: FieldMetadataType.DATE, + isNullable: true, + isActive: true, + isCustom: false, + isSystem: false, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + icon: 'IconCalendarEvent', + description: null, + }, + { + id: '18', + name: 'dateTimeField', + label: 'Date Time Field', + type: FieldMetadataType.DATE_TIME, + isNullable: true, + isActive: true, + isCustom: false, + isSystem: false, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + icon: 'IconCalendarClock', + description: null, + }, + { + id: '19', + name: 'ratingField', + label: 'Rating Field', + type: FieldMetadataType.RATING, + isNullable: true, + isActive: true, + isCustom: false, + isSystem: false, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + icon: 'IconStar', + description: null, + }, + { + id: '20', + name: 'emailField', + label: 'Email Field', + type: FieldMetadataType.EMAILS, + isNullable: true, + isActive: true, + isCustom: false, + isSystem: false, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + icon: 'IconMail', + description: null, + }, + ]; it('should successfully build a record from imported structured row', () => { const importedStructuredRow: ImportedStructuredRow = { booleanField: 'true', @@ -48,303 +344,6 @@ describe('buildRecordFromImportedStructuredRow', () => { '[{"url": "https://github.com/user", "label": "GitHub"}]', }; - const fields: FieldMetadataItem[] = [ - { - id: '3', - name: 'booleanField', - label: 'Boolean Field', - type: FieldMetadataType.BOOLEAN, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconCheck', - description: null, - }, - { - id: '4', - name: 'numberField', - label: 'Number Field', - type: FieldMetadataType.NUMBER, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconNumber', - description: null, - }, - { - id: '5', - name: 'multiSelectField', - label: 'Multi-Select Field', - type: FieldMetadataType.MULTI_SELECT, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconTag', - description: null, - options: [ - { - id: '1', - value: 'tag1', - label: 'Tag 1', - color: 'blue', - position: 0, - }, - { - id: '2', - value: 'tag2', - label: 'Tag 2', - color: 'red', - position: 1, - }, - { - id: '3', - value: 'tag3', - label: 'Tag 3', - color: 'green', - position: 2, - }, - ], - }, - { - id: '6', - name: 'relationField', - label: 'Relation Field', - type: FieldMetadataType.RELATION, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconBuilding', - description: null, - relation: { - type: RelationType.MANY_TO_ONE, - } as FieldMetadataItemRelation, - }, - { - id: '7', - name: 'fullNameField', - label: 'Full Name Field', - type: FieldMetadataType.FULL_NAME, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconUser', - description: null, - }, - { - id: '8', - name: 'currencyField', - label: 'Currency Field', - type: FieldMetadataType.CURRENCY, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconCurrencyDollar', - description: null, - }, - { - id: '9', - name: 'addressField', - label: 'Address Field', - type: FieldMetadataType.ADDRESS, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconMap', - description: null, - }, - { - id: '10', - name: 'selectField', - label: 'Select Field', - type: FieldMetadataType.SELECT, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconTag', - description: null, - options: [ - { - id: '1', - value: 'option1', - label: 'Option 1', - color: 'blue', - position: 0, - }, - { - id: '2', - value: 'option2', - label: 'Option 2', - color: 'red', - position: 1, - }, - ], - }, - { - id: '11', - name: 'arrayField', - label: 'Array Field', - type: FieldMetadataType.ARRAY, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconBracketsContain', - description: null, - }, - { - id: '12', - name: 'jsonField', - label: 'JSON Field', - type: FieldMetadataType.RAW_JSON, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconBraces', - description: null, - }, - { - id: '13', - name: 'phoneField', - label: 'Phone Field', - type: FieldMetadataType.PHONES, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconPhone', - description: null, - }, - { - id: '14', - name: 'linksField', - label: 'Links Field', - type: FieldMetadataType.LINKS, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconWorld', - description: null, - }, - { - id: '15', - name: 'createdBy', - label: 'Created by', - type: FieldMetadataType.ACTOR, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconUsers', - description: null, - }, - { - id: '16', - name: 'richTextField', - label: 'Rich Text Field', - type: FieldMetadataType.RICH_TEXT_V2, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconTextEditor', - description: null, - }, - { - id: '17', - name: 'dateField', - label: 'Date Field', - type: FieldMetadataType.DATE, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconCalendarEvent', - description: null, - }, - { - id: '18', - name: 'dateTimeField', - label: 'Date Time Field', - type: FieldMetadataType.DATE_TIME, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconCalendarClock', - description: null, - }, - { - id: '19', - name: 'ratingField', - label: 'Rating Field', - type: FieldMetadataType.RATING, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconStar', - description: null, - }, - { - id: '20', - name: 'emailField', - label: 'Email Field', - type: FieldMetadataType.EMAILS, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconMail', - description: null, - }, - ]; - const spreadsheetImportFields = [ { fieldMetadataItemId: '6', @@ -381,7 +380,6 @@ describe('buildRecordFromImportedStructuredRow', () => { }, }, }, - relationFieldId: undefined, selectField: 'option1', arrayField: ['item1', 'item2', 'item3'], jsonField: { key: 'value', nested: { prop: 'data' } }, @@ -401,6 +399,10 @@ describe('buildRecordFromImportedStructuredRow', () => { addressState: 'NY', addressCountry: 'USA', }, + createdBy: { + source: 'IMPORT', + context: {}, + }, phoneField: { primaryPhoneNumber: '+1-555-0123', primaryPhoneCountryCode: 'US', @@ -423,10 +425,6 @@ describe('buildRecordFromImportedStructuredRow', () => { }, ], }, - createdBy: { - source: 'IMPORT', - context: {}, - }, richTextField: { blocknote: 'Rich content in blocknote format', markdown: 'Content in markdown format', @@ -442,23 +440,6 @@ describe('buildRecordFromImportedStructuredRow', () => { 'Primary Phone Number (phoneField)': '5550123', }; - const fields: FieldMetadataItem[] = [ - { - id: '13', - name: 'phoneField', - label: 'Phone Field', - type: FieldMetadataType.PHONES, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconPhone', - description: null, - }, - ]; - const result = buildRecordFromImportedStructuredRow({ importedStructuredRow, fieldMetadataItems: fields, @@ -470,6 +451,10 @@ describe('buildRecordFromImportedStructuredRow', () => { primaryPhoneNumber: '5550123', primaryPhoneCallingCode: '+1', }, + createdBy: { + source: 'IMPORT', + context: {}, + }, }); }); @@ -478,26 +463,6 @@ describe('buildRecordFromImportedStructuredRow', () => { 'emailField (relationField)': 'john.doe@example.com', }; - const fields: FieldMetadataItem[] = [ - { - id: '6', - name: 'relationField', - label: 'Relation Field', - type: FieldMetadataType.RELATION, - isNullable: true, - isActive: true, - isCustom: false, - isSystem: false, - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - icon: 'IconBuilding', - description: null, - relation: { - type: RelationType.MANY_TO_ONE, - } as FieldMetadataItemRelation, - }, - ]; - const spreadsheetImportFields = [ { fieldMetadataItemId: '6', @@ -530,6 +495,42 @@ describe('buildRecordFromImportedStructuredRow', () => { }, }, }, + createdBy: { + source: 'IMPORT', + context: {}, + }, + }); + }); + + it('should return empty record for empty imported row', () => { + const importedStructuredRow: ImportedStructuredRow = {}; + + const spreadsheetImportFields = [ + { + fieldMetadataItemId: '6', + isNestedField: false, + isRelationConnectField: true, + label: 'Relation Field / Name Field', + key: 'nameField (relationField)', + fieldMetadataType: FieldMetadataType.RELATION, + uniqueFieldMetadataItem: { + name: 'nameField', + type: FieldMetadataType.TEXT, + }, + }, + ] as SpreadsheetImportField[]; + + const result = buildRecordFromImportedStructuredRow({ + importedStructuredRow, + fieldMetadataItems: fields, + spreadsheetImportFields, + }); + + expect(result).toEqual({ + createdBy: { + source: 'IMPORT', + context: {}, + }, }); }); }); diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts index 9dc5d34347..df7671f8c9 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts @@ -287,19 +287,27 @@ export const buildRecordFromImportedStructuredRow = ({ break; } case FieldMetadataType.BOOLEAN: - recordToBuild[field.name] = - importedFieldValue === 'true' || importedFieldValue === true; + if (isDefined(importedFieldValue)) { + recordToBuild[field.name] = + importedFieldValue === 'true' || importedFieldValue === true; + } break; case FieldMetadataType.NUMBER: case FieldMetadataType.NUMERIC: - recordToBuild[field.name] = Number(importedFieldValue); + if (isDefined(importedFieldValue)) { + recordToBuild[field.name] = Number(importedFieldValue); + } break; case FieldMetadataType.RELATION: { - recordToBuild[field.name] = buildRelationConnectFieldRecord( + const relationConnectFieldValue = buildRelationConnectFieldRecord( field, importedStructuredRow, spreadsheetImportFields, ); + if (isDefined(relationConnectFieldValue)) { + recordToBuild[field.name] = relationConnectFieldValue; + } + break; } case FieldMetadataType.ACTOR: @@ -310,8 +318,10 @@ export const buildRecordFromImportedStructuredRow = ({ break; case FieldMetadataType.ARRAY: case FieldMetadataType.MULTI_SELECT: { - recordToBuild[field.name] = - stringArrayJSONSchema.parse(importedFieldValue); + if (isDefined(importedFieldValue)) { + recordToBuild[field.name] = + stringArrayJSONSchema.parse(importedFieldValue); + } break; } case FieldMetadataType.RAW_JSON: {