From 741924751b68525c977cd933b44ac1d73a51c809 Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Fri, 25 Jul 2025 09:48:17 +0200 Subject: [PATCH] Connect - Import Relation (#13419) re-opened https://github.com/twentyhq/twenty/pull/13213 --- .../useBuildSpreadSheetImportFields.test.tsx | 503 ++++++++++++++++++ ...jectRecordsSpreadsheetImportDialog.test.ts | 8 +- .../hooks/useBuildAvailableFieldsForImport.ts | 177 ------ .../hooks/useBuildSpreadSheetImportFields.ts | 285 ++++++++++ ...penObjectRecordsSpreadsheetImportDialog.ts | 29 +- .../types/AvailableFieldForImport.ts | 15 - ...ildRecordFromImportedStructuredRow.test.ts | 108 +++- ...spreadsheetImportGetUnicityRowHook.test.ts | 8 +- .../buildRecordFromImportedStructuredRow.ts | 126 ++++- ...preadSheetGetRelationConnectSubFieldKey.ts | 10 + ...mportFilterAvailableFieldMetadataItems.ts} | 0 ...readsheetImportGetCompositeSubFieldKey.ts} | 8 +- ...GetCompositeSubFieldLabelWithFieldLabel.ts | 8 + ...etImportGetRelationConnectSubFieldLabel.ts | 20 + .../spreadsheetImportGetUnicityRowHook.ts | 38 +- .../utils/sanitizeRecordInput.ts | 14 +- .../SettingsCompositeFieldTypeConfigs.ts | 6 +- .../__mocks__/mockRsiValues.ts | 35 +- ...ColumnSelectFieldSelectDropdownContent.tsx | 4 +- ...umnSelectSubFieldSelectDropdownContent.tsx | 78 +-- .../components/MatchColumnToFieldSelect.tsx | 18 +- .../ReactSpreadsheetImportContextProvider.tsx | 10 +- .../__tests__/useSpreadsheetImport.test.tsx | 64 ++- ...useComputeColumnSuggestionsAndAutoMatch.ts | 5 +- .../hooks/useOpenSpreadsheetImportDialog.ts | 4 +- .../hooks/useSpreadsheetImportInternal.ts | 4 +- .../provider/components/SpreadsheetImport.tsx | 10 +- .../states/spreadsheetImportDialogState.ts | 23 +- .../steps/components/ImportDataStep.tsx | 4 - .../MatchColumnsStep/MatchColumnsStep.tsx | 16 +- .../components/ColumnGrid.tsx | 14 +- .../SubMatchingSelectDropdownButton.tsx | 14 +- .../components/SubMatchingSelectRow.tsx | 18 +- .../SubMatchingSelectRowLeftSelect.tsx | 8 +- .../SubMatchingSelectRowRightDropdown.tsx | 22 +- .../components/TemplateColumn.tsx | 20 +- .../components/UnmatchColumn.tsx | 18 +- .../components/UserTableColumn.tsx | 8 +- .../states/initialComputedColumnsState.ts | 10 +- .../suggestedFieldsByColumnHeaderState.ts | 2 +- .../UploadStep/components/columns.tsx | 58 -- .../hooks/useDownloadFakeRecords.ts | 7 +- .../ValidationStep/ValidationStep.tsx | 41 +- .../ValidationStep/components/columns.tsx | 14 +- .../steps/types/SpreadsheetImportStep.ts | 2 +- .../types/SpreadsheetColumn.ts | 36 +- .../types/SpreadsheetColumns.ts | 2 +- .../types/SpreadsheetImportDialogOptions.ts | 17 +- .../types/SpreadsheetImportField.ts | 21 +- .../types/SpreadsheetImportFieldOption.ts | 12 + .../types/SpreadsheetImportFields.ts | 4 +- ...SpreadsheetImportImportValidationResult.ts | 9 +- .../SpreadsheetImportImportedStructuredRow.ts | 4 +- .../types/SpreadsheetImportRowHook.ts | 10 +- .../types/SpreadsheetImportTableHook.ts | 8 +- .../types/SpreadsheetMatchedOptions.ts | 4 +- .../utils/__tests__/dataMutations.test.ts | 57 +- .../utils/__tests__/findMatch.test.ts | 93 ---- .../findUnmatchedRequiredFields.test.ts | 20 +- .../utils/__tests__/getFieldOptions.test.ts | 6 +- .../utils/__tests__/getMatchedColumns.test.ts | 166 ------ .../__tests__/normalizeTableData.test.ts | 24 +- .../utils/__tests__/setColumn.test.ts | 14 +- .../utils/__tests__/setIgnoreColumn.test.ts | 2 +- .../utils/__tests__/setSubColumn.test.ts | 4 +- .../spreadsheet-import/utils/dataMutations.ts | 31 +- .../spreadsheet-import/utils/findMatch.ts | 30 -- .../utils/findUnmatchedRequiredFields.ts | 6 +- .../utils/getFieldOptions.ts | 4 +- .../utils/getMatchedColumns.ts | 52 -- .../utils/getMatchedColumnsWithFuse.ts | 14 +- .../utils/getShortNestedFieldLabel.ts | 3 + .../utils/normalizeTableData.ts | 8 +- .../spreadsheet-import/utils/setColumn.ts | 18 +- .../utils/setIgnoreColumn.ts | 4 +- .../spreadsheet-import/utils/setSubColumn.ts | 14 +- .../utils/spreadsheetBuildFieldOptions.ts | 29 - .../spreadsheetImportBuildFieldOptions.ts | 42 ++ .../spreadsheetImportGetSubFieldOptions.ts | 14 + .../utils/spreadsheetImportHasNestedFields.ts | 12 + .../spreadsheet-import/utils/uniqueEntries.ts | 4 +- ...n-connect-input-type-definition.factory.ts | 2 +- .../full-name.composite-type.ts | 4 +- .../relation-nested-queries.ts | 14 +- ...ectRecordNotFoundErrorMessage.util.spec.ts | 21 + ...tConnectRecordNotFoundErrorMessage.util.ts | 18 + ...ute-relation-connect-query-configs.util.ts | 3 +- ...ested-relation-queries.integration-spec.ts | 2 +- packages/twenty-shared/src/utils/index.ts | 1 + .../getUniqueConstraintsFields.test.ts} | 5 +- .../getUniqueConstraintsFields.ts} | 2 +- .../src/utils/indexMetadata/index.ts | 1 + 92 files changed, 1612 insertions(+), 1153 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useBuildSpreadSheetImportFields.test.tsx delete mode 100644 packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts create mode 100644 packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildSpreadSheetImportFields.ts delete mode 100644 packages/twenty-front/src/modules/object-record/spreadsheet-import/types/AvailableFieldForImport.ts create mode 100644 packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadSheetGetRelationConnectSubFieldKey.ts rename packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/{spreadsheetImportFilterAvailableFieldMetadataItems.ts.ts => spreadsheetImportFilterAvailableFieldMetadataItems.ts} (100%) rename packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/{getSubFieldOptionKey.ts => spreadsheetImportGetCompositeSubFieldKey.ts} (69%) create mode 100644 packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldLabelWithFieldLabel.ts create mode 100644 packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetRelationConnectSubFieldLabel.ts delete mode 100644 packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/columns.tsx create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFieldOption.ts delete mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findMatch.test.ts delete mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getMatchedColumns.test.ts delete mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/findMatch.ts delete mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumns.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/getShortNestedFieldLabel.ts delete mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetBuildFieldOptions.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportBuildFieldOptions.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportGetSubFieldOptions.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportHasNestedFields.ts create mode 100644 packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/utils/__tests__/formatConnectRecordNotFoundErrorMessage.util.spec.ts create mode 100644 packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/utils/formatConnectRecordNotFoundErrorMessage.util.ts rename packages/{twenty-server/src/engine/metadata-modules/index-metadata/utils/__tests__/getUniqueConstraintsFields.util.spec.ts => twenty-shared/src/utils/indexMetadata/__tests__/getUniqueConstraintsFields.test.ts} (94%) rename packages/{twenty-server/src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util.ts => twenty-shared/src/utils/indexMetadata/getUniqueConstraintsFields.ts} (95%) create mode 100644 packages/twenty-shared/src/utils/indexMetadata/index.ts diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useBuildSpreadSheetImportFields.test.tsx b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useBuildSpreadSheetImportFields.test.tsx new file mode 100644 index 000000000..def0d25dc --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useBuildSpreadSheetImportFields.test.tsx @@ -0,0 +1,503 @@ +import { renderHook } from '@testing-library/react'; +import { ReactNode } from 'react'; +import { RecoilRoot, useSetRecoilState } from 'recoil'; +import { useIcons } from 'twenty-ui/display'; +import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter'; + +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { IndexMetadataItem } from '@/object-metadata/types/IndexMetadataItem'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useBuildSpreadsheetImportFields } from '@/object-record/spreadsheet-import/hooks/useBuildSpreadSheetImportFields'; +import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql'; + +const Wrapper = ({ children }: { children: ReactNode }) => { + return ( + + {children} + + ); +}; + +jest.mock('twenty-ui/display', () => ({ + useIcons: jest.fn(), +})); + +describe('useBuildSpreadSheetImportFields', () => { + const mockGetIcon = jest.fn().mockReturnValue('MockIcon'); + const mockUseIcons = useIcons as jest.MockedFunction; + + beforeEach(() => { + mockUseIcons.mockReturnValue({ + getIcon: mockGetIcon, + getIcons: () => ({}), + }); + jest.clearAllMocks(); + }); + + const createMockFieldMetadataItem = ( + overrides: Partial = {}, + ): FieldMetadataItem => ({ + id: 'test-field-id', + name: 'testField', + label: 'Test Field', + type: FieldMetadataType.TEXT, + icon: 'IconTest', + isActive: true, + isCustom: false, + isSystem: false, + isNullable: true, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + ...overrides, + }); + + const createMockObjectMetadataItem = ( + overrides: Partial = {}, + ): ObjectMetadataItem => + ({ + id: 'test-object-id', + nameSingular: 'testObject', + namePlural: 'testObjects', + labelSingular: 'Test Object', + labelPlural: 'Test Objects', + description: 'Test object description', + icon: 'IconTest', + isCustom: false, + isSystem: false, + isActive: true, + isLabelSyncedWithName: false, + isRemote: false, + isSearchable: true, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + fields: [], + ...overrides, + }) as ObjectMetadataItem; + + it('should build importFields for basic field types', () => { + const { result } = renderHook( + () => { + const setObjectMetadataItems = useSetRecoilState( + objectMetadataItemsState, + ); + setObjectMetadataItems([]); + + return useBuildSpreadsheetImportFields(); + }, + { wrapper: Wrapper }, + ); + + const fieldMetadataItems: FieldMetadataItem[] = [ + createMockFieldMetadataItem({ + type: FieldMetadataType.TEXT, + name: 'textField', + label: 'Text Field', + }), + createMockFieldMetadataItem({ + type: FieldMetadataType.NUMBER, + name: 'numberField', + label: 'Number Field', + }), + createMockFieldMetadataItem({ + type: FieldMetadataType.BOOLEAN, + name: 'booleanField', + label: 'Boolean Field', + }), + ]; + + const spreadsheetImportFields = + result.current.buildSpreadsheetImportFields(fieldMetadataItems); + + expect(spreadsheetImportFields).toHaveLength(3); + + expect(spreadsheetImportFields[0]).toMatchObject({ + label: 'Text Field', + key: 'textField', + fieldType: { type: 'input' }, + fieldMetadataType: FieldMetadataType.TEXT, + isNestedField: false, + }); + + expect(spreadsheetImportFields[1]).toMatchObject({ + label: 'Number Field', + key: 'numberField', + fieldType: { type: 'input' }, + fieldMetadataType: FieldMetadataType.NUMBER, + isNestedField: false, + }); + + expect(spreadsheetImportFields[2]).toMatchObject({ + label: 'Boolean Field', + key: 'booleanField', + fieldType: { type: 'checkbox' }, + fieldMetadataType: FieldMetadataType.BOOLEAN, + isNestedField: false, + }); + }); + + it('should build importFields for select types', () => { + const { result } = renderHook( + () => { + const setObjectMetadataItems = useSetRecoilState( + objectMetadataItemsState, + ); + setObjectMetadataItems([]); + + return useBuildSpreadsheetImportFields(); + }, + { wrapper: Wrapper }, + ); + + const fieldMetadataItems: FieldMetadataItem[] = [ + createMockFieldMetadataItem({ + type: FieldMetadataType.SELECT, + name: 'selectField', + label: 'Select Field', + options: [ + { + id: '1', + label: 'Option 1', + value: 'opt1', + color: 'red', + position: 0, + }, + { + id: '2', + label: 'Option 2', + value: 'opt2', + color: 'blue', + position: 1, + }, + ], + }), + createMockFieldMetadataItem({ + type: FieldMetadataType.MULTI_SELECT, + name: 'multiSelectField', + label: 'Multi Select Field', + options: [ + { + id: '1', + label: 'Tag 1', + value: 'tag1', + color: 'green', + position: 0, + }, + { + id: '2', + label: 'Tag 2', + value: 'tag2', + color: 'yellow', + position: 1, + }, + ], + }), + ]; + + const spreadsheetImportFields = + result.current.buildSpreadsheetImportFields(fieldMetadataItems); + + expect(spreadsheetImportFields).toHaveLength(2); + + expect(spreadsheetImportFields[0]).toMatchObject({ + label: 'Select Field', + key: 'selectField', + fieldType: { + type: 'select', + options: [ + { label: 'Option 1', value: 'opt1', color: 'red' }, + { label: 'Option 2', value: 'opt2', color: 'blue' }, + ], + }, + fieldMetadataType: FieldMetadataType.SELECT, + }); + + expect(spreadsheetImportFields[1]).toMatchObject({ + label: 'Multi Select Field', + key: 'multiSelectField', + fieldType: { + type: 'multiSelect', + options: [ + { label: 'Tag 1', value: 'tag1', color: 'green' }, + { label: 'Tag 2', value: 'tag2', color: 'yellow' }, + ], + }, + fieldMetadataType: FieldMetadataType.MULTI_SELECT, + }); + }); + + it('should build importFields for composite types (full name)', () => { + const { result } = renderHook( + () => { + const setObjectMetadataItems = useSetRecoilState( + objectMetadataItemsState, + ); + setObjectMetadataItems([]); + + return useBuildSpreadsheetImportFields(); + }, + { wrapper: Wrapper }, + ); + + const fieldMetadataItems: FieldMetadataItem[] = [ + createMockFieldMetadataItem({ + type: FieldMetadataType.FULL_NAME, + name: 'fullName', + label: 'Full Name', + }), + ]; + + const spreadsheetImportFields = + result.current.buildSpreadsheetImportFields(fieldMetadataItems); + + expect(spreadsheetImportFields.length).toBe(2); + + const firstNameField = spreadsheetImportFields.find((field) => + field.key.includes('First Name'), + ); + const lastNameField = spreadsheetImportFields.find((field) => + field.key.includes('Last Name'), + ); + + expect(firstNameField).toBeDefined(); + expect(lastNameField).toBeDefined(); + + expect(firstNameField?.isNestedField).toBe(true); + expect(firstNameField?.isCompositeSubField).toBe(true); + expect(lastNameField?.isNestedField).toBe(true); + expect(lastNameField?.isCompositeSubField).toBe(true); + }); + + it('should filter out ACTOR fields', () => { + const { result } = renderHook( + () => { + const setObjectMetadataItems = useSetRecoilState( + objectMetadataItemsState, + ); + setObjectMetadataItems([]); + + return useBuildSpreadsheetImportFields(); + }, + { wrapper: Wrapper }, + ); + + const fieldMetadataItems: FieldMetadataItem[] = [ + createMockFieldMetadataItem({ + type: FieldMetadataType.ACTOR, + name: 'actorField', + label: 'Actor Field', + }), + createMockFieldMetadataItem({ + type: FieldMetadataType.TEXT, + name: 'textField', + label: 'Text Field', + }), + ]; + + const spreadsheetImportFields = + result.current.buildSpreadsheetImportFields(fieldMetadataItems); + + expect(spreadsheetImportFields).toHaveLength(1); + expect(spreadsheetImportFields[0].fieldMetadataType).toBe( + FieldMetadataType.TEXT, + ); + }); + + it('should return empty array for unsupported field types', () => { + const { result } = renderHook( + () => { + const setObjectMetadataItems = useSetRecoilState( + objectMetadataItemsState, + ); + setObjectMetadataItems([]); + + return useBuildSpreadsheetImportFields(); + }, + { wrapper: Wrapper }, + ); + + const fieldMetadataItems: FieldMetadataItem[] = [ + createMockFieldMetadataItem({ + type: FieldMetadataType.POSITION, + name: 'positionField', + label: 'Position Field', + }), + createMockFieldMetadataItem({ + type: FieldMetadataType.TS_VECTOR, + name: 'tsVectorField', + label: 'TS Vector Field', + }), + ]; + + const spreadsheetImportFields = + result.current.buildSpreadsheetImportFields(fieldMetadataItems); + + expect(spreadsheetImportFields).toHaveLength(0); + }); + + it('should build importFields for relation field type', () => { + const { result } = renderHook( + () => { + const setObjectMetadataItems = useSetRecoilState( + objectMetadataItemsState, + ); + + const targetObjectMetadata = createMockObjectMetadataItem({ + id: 'target-object-id', + nameSingular: 'company', + namePlural: 'companies', + labelSingular: 'Company', + labelPlural: 'Companies', + fields: [ + createMockFieldMetadataItem({ + id: 'company-id-field', + name: 'id', + label: 'ID', + type: FieldMetadataType.UUID, + }), + createMockFieldMetadataItem({ + id: 'company-name-field', + name: 'name', + label: 'Name', + type: FieldMetadataType.TEXT, + }), + createMockFieldMetadataItem({ + id: 'company-email-field', + name: 'emails', + label: 'Emails', + type: FieldMetadataType.EMAILS, + }), + ], + indexMetadatas: [ + { + id: 'primary-key-index', + name: 'primaryKeyIndex', + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + isUnique: true, + indexFieldMetadatas: [ + { + id: 'index-field-1', + fieldMetadataId: 'company-id-field', + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + order: 0, + }, + ], + }, + { + id: 'unique-name-index', + name: 'uniqueNameIndex', + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + isUnique: true, + indexFieldMetadatas: [ + { + id: 'index-field-2', + fieldMetadataId: 'company-name-field', + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + order: 0, + }, + ], + }, + { + id: 'unique-email-index', + name: 'uniqueEmailIndex', + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + isUnique: true, + indexFieldMetadatas: [ + { + id: 'index-field-3', + fieldMetadataId: 'company-email-field', + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + order: 0, + }, + ], + }, + ] as IndexMetadataItem[], + }); + + setObjectMetadataItems([targetObjectMetadata]); + + return useBuildSpreadsheetImportFields(); + }, + { wrapper: Wrapper }, + ); + + const fieldMetadataItems: FieldMetadataItem[] = [ + createMockFieldMetadataItem({ + type: FieldMetadataType.RELATION, + name: 'company', + label: 'Company', + relation: { + type: RelationType.MANY_TO_ONE, + targetObjectMetadata: { + id: 'target-object-id', + nameSingular: 'company', + namePlural: 'companies', + }, + } as any, + }), + ]; + + const spreadsheetImportFields = + result.current.buildSpreadsheetImportFields(fieldMetadataItems); + + expect(spreadsheetImportFields).toHaveLength(4); + + const idField = spreadsheetImportFields.find((field) => + field.key.includes('id (company)'), + ); + expect(idField).toBeDefined(); + expect(idField).toMatchObject({ + label: 'Company / ID', + key: 'id (company)', + fieldMetadataItemId: 'test-field-id', + fieldMetadataType: FieldMetadataType.RELATION, + isNestedField: true, + isRelationConnectField: true, + uniqueFieldMetadataItem: { + id: 'company-id-field', + name: 'id', + type: FieldMetadataType.UUID, + }, + }); + + const nameField = spreadsheetImportFields.find((field) => + field.key.includes('name (company)'), + ); + expect(nameField).toBeDefined(); + expect(nameField).toMatchObject({ + label: 'Company / Name', + key: 'name (company)', + fieldMetadataItemId: 'test-field-id', + fieldMetadataType: FieldMetadataType.RELATION, + isNestedField: true, + isRelationConnectField: true, + uniqueFieldMetadataItem: { + id: 'company-name-field', + name: 'name', + type: FieldMetadataType.TEXT, + }, + }); + + const primaryEmailField = spreadsheetImportFields.find((field) => + field.key.includes('primaryEmail-emails (company)'), + ); + expect(primaryEmailField).toBeDefined(); + expect(primaryEmailField).toMatchObject({ + isNestedField: true, + isCompositeSubField: true, + isRelationConnectField: true, + compositeSubFieldKey: 'primaryEmail', + uniqueFieldMetadataItem: { + id: 'company-email-field', + name: 'emails', + type: FieldMetadataType.EMAILS, + }, + }); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useOpenObjectRecordsSpreadsheetImportDialog.test.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useOpenObjectRecordsSpreadsheetImportDialog.test.ts index 6a345c09f..8c9d3bfa2 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useOpenObjectRecordsSpreadsheetImportDialog.test.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useOpenObjectRecordsSpreadsheetImportDialog.test.ts @@ -383,9 +383,13 @@ describe('useSpreadsheetCompanyImport', () => { expect(spreadsheetImportDialogAfterOpen.options?.onSubmit).toBeInstanceOf( Function, ); - expect(spreadsheetImportDialogAfterOpen.options).toHaveProperty('fields'); + expect(spreadsheetImportDialogAfterOpen.options).toHaveProperty( + 'spreadsheetImportFields', + ); expect( - Array.isArray(spreadsheetImportDialogAfterOpen.options?.fields), + Array.isArray( + spreadsheetImportDialogAfterOpen.options?.spreadsheetImportFields, + ), ).toBe(true); act(() => { 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 deleted file mode 100644 index 135176e76..000000000 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; - -import { AvailableFieldForImport } from '@/object-record/spreadsheet-import/types/AvailableFieldForImport'; -import { getSpreadSheetFieldValidationDefinitions } from '@/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions'; -import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; -import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType'; -import { useIcons } from 'twenty-ui/display'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; - -export const useBuildAvailableFieldsForImport = () => { - const { getIcon } = useIcons(); - - const buildAvailableFieldsForImport = ( - fieldMetadataItems: FieldMetadataItem[], - ) => { - const availableFieldsForImport: AvailableFieldForImport[] = []; - - const createBaseField = ( - fieldMetadataItem: FieldMetadataItem, - overrides: Partial = {}, - customLabel?: string, - ): AvailableFieldForImport => ({ - Icon: getIcon(fieldMetadataItem.icon), - label: customLabel ?? fieldMetadataItem.label, - key: fieldMetadataItem.name, - fieldType: { type: 'input' }, - fieldMetadataType: fieldMetadataItem.type, - fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions( - fieldMetadataItem.type, - customLabel ?? fieldMetadataItem.label, - ), - ...overrides, - }); - - const handleCompositeFieldWithLabels = ( - fieldMetadataItem: FieldMetadataItem, - fieldType: CompositeFieldType, - ) => { - SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldType].subFields.forEach( - ({ subFieldName, subFieldLabel, isImportable }) => { - if (!isImportable) return; - const label = `${fieldMetadataItem.label} / ${subFieldLabel}`; - - availableFieldsForImport.push( - createBaseField(fieldMetadataItem, { - label, - key: `${subFieldLabel} (${fieldMetadataItem.name})`, - fieldValidationDefinitions: - getSpreadSheetFieldValidationDefinitions( - fieldMetadataItem.type, - label, - subFieldName, - ), - }), - ); - }, - ); - }; - - const handleSelectField = ( - fieldMetadataItem: FieldMetadataItem, - isMulti = false, - ) => { - availableFieldsForImport.push( - createBaseField(fieldMetadataItem, { - fieldType: { - type: isMulti ? 'multiSelect' : 'select', - options: - fieldMetadataItem.options?.map((option) => ({ - label: option.label, - value: option.value, - color: option.color, - })) || [], - }, - fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions( - fieldMetadataItem.type, - `${fieldMetadataItem.label} (ID)`, - ), - }), - ); - }; - - const fieldTypeHandlers: Record< - string, - (fieldMetadataItem: FieldMetadataItem) => void - > = { - [FieldMetadataType.FULL_NAME]: (fieldMetadataItem) => { - handleCompositeFieldWithLabels( - fieldMetadataItem, - FieldMetadataType.FULL_NAME, - ); - }, - [FieldMetadataType.ADDRESS]: (fieldMetadataItem) => { - handleCompositeFieldWithLabels( - fieldMetadataItem, - FieldMetadataType.ADDRESS, - ); - }, - [FieldMetadataType.LINKS]: (fieldMetadataItem) => { - handleCompositeFieldWithLabels( - fieldMetadataItem, - FieldMetadataType.LINKS, - ); - }, - [FieldMetadataType.EMAILS]: (fieldMetadataItem) => { - handleCompositeFieldWithLabels( - fieldMetadataItem, - FieldMetadataType.EMAILS, - ); - }, - [FieldMetadataType.PHONES]: (fieldMetadataItem) => { - handleCompositeFieldWithLabels( - fieldMetadataItem, - FieldMetadataType.PHONES, - ); - }, - [FieldMetadataType.RICH_TEXT_V2]: (fieldMetadataItem) => { - handleCompositeFieldWithLabels( - fieldMetadataItem, - FieldMetadataType.RICH_TEXT_V2, - ); - }, - [FieldMetadataType.CURRENCY]: (fieldMetadataItem) => { - handleCompositeFieldWithLabels( - fieldMetadataItem, - FieldMetadataType.CURRENCY, - ); - }, - [FieldMetadataType.ACTOR]: (fieldMetadataItem) => { - handleCompositeFieldWithLabels( - fieldMetadataItem, - FieldMetadataType.ACTOR, - ); - }, - [FieldMetadataType.RELATION]: (fieldMetadataItem) => { - const label = `${fieldMetadataItem.label} (ID)`; - availableFieldsForImport.push( - createBaseField(fieldMetadataItem, { - label, - fieldValidationDefinitions: - getSpreadSheetFieldValidationDefinitions( - fieldMetadataItem.type, - label, - ), - }), - ); - }, - [FieldMetadataType.SELECT]: (fieldMetadataItem) => { - handleSelectField(fieldMetadataItem, false); - }, - [FieldMetadataType.MULTI_SELECT]: (fieldMetadataItem) => { - handleSelectField(fieldMetadataItem, true); - }, - [FieldMetadataType.BOOLEAN]: (fieldMetadataItem) => { - availableFieldsForImport.push( - createBaseField(fieldMetadataItem, { - fieldType: { type: 'checkbox' }, - }), - ); - }, - - default: (fieldMetadataItem) => { - availableFieldsForImport.push(createBaseField(fieldMetadataItem)); - }, - }; - - for (const fieldMetadataItem of fieldMetadataItems) { - const handler = - fieldTypeHandlers[fieldMetadataItem.type] || fieldTypeHandlers.default; - handler(fieldMetadataItem); - } - - return availableFieldsForImport; - }; - - return { buildAvailableFieldsForImport }; -}; diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildSpreadSheetImportFields.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildSpreadSheetImportFields.ts new file mode 100644 index 000000000..762d0c907 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildSpreadSheetImportFields.ts @@ -0,0 +1,285 @@ +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; + +import { getSpreadSheetFieldValidationDefinitions } from '@/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions'; +import { getRelationConnectSubFieldKey } from '@/object-record/spreadsheet-import/utils/spreadSheetGetRelationConnectSubFieldKey'; +import { getCompositeSubFieldKey } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldKey'; +import { getCompositeSubFieldLabelWithFieldLabel } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldLabelWithFieldLabel'; +import { getRelationConnectSubFieldLabel } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetRelationConnectSubFieldLabel'; +import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; +import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType'; +import { + SpreadsheetImportField, + SpreadsheetImportFields, +} from '@/spreadsheet-import/types'; +import { useRecoilValue } from 'recoil'; +import { + assertUnreachable, + getUniqueConstraintsFields, + isDefined, +} from 'twenty-shared/utils'; +import { useIcons } from 'twenty-ui/display'; +import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql'; + +export const useBuildSpreadsheetImportFields = () => { + const { getIcon } = useIcons(); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + + const buildSpreadsheetImportFields = ( + fieldMetadataItems: FieldMetadataItem[], + ): SpreadsheetImportFields => { + return fieldMetadataItems + .filter((field) => field.type !== FieldMetadataType.ACTOR) + .flatMap((fieldMetadataItem) => + buildSpreadsheetImportField(fieldMetadataItem), + ); + }; + + const buildSpreadsheetImportField = ( + fieldMetadataItem: FieldMetadataItem, + relationConnectFieldOverrides?: Partial, + ) => { + switch (fieldMetadataItem.type) { + case FieldMetadataType.ADDRESS: + case FieldMetadataType.CURRENCY: + case FieldMetadataType.EMAILS: + case FieldMetadataType.FULL_NAME: + case FieldMetadataType.LINKS: + case FieldMetadataType.PHONES: + case FieldMetadataType.RICH_TEXT_V2: + return handleCompositeFields({ + fieldMetadataItem, + fieldType: fieldMetadataItem.type, + }); + case FieldMetadataType.RELATION: + return handleRelationField(fieldMetadataItem); + case FieldMetadataType.SELECT: + case FieldMetadataType.MULTI_SELECT: + return [ + handleSelectField( + fieldMetadataItem, + fieldMetadataItem.type === FieldMetadataType.MULTI_SELECT, + relationConnectFieldOverrides, + ), + ]; + case FieldMetadataType.BOOLEAN: + return [ + createBaseField(fieldMetadataItem, { + fieldType: { type: 'checkbox' }, + ...(isDefined(relationConnectFieldOverrides) + ? relationConnectFieldOverrides + : {}), + }), + ]; + case FieldMetadataType.DATE_TIME: + case FieldMetadataType.DATE: + case FieldMetadataType.NUMBER: + case FieldMetadataType.NUMERIC: + case FieldMetadataType.TEXT: + case FieldMetadataType.UUID: + case FieldMetadataType.ARRAY: + case FieldMetadataType.RATING: + case FieldMetadataType.RAW_JSON: + return [ + createBaseField(fieldMetadataItem, relationConnectFieldOverrides), + ]; + + case FieldMetadataType.POSITION: + case FieldMetadataType.MORPH_RELATION: + case FieldMetadataType.ACTOR: + case FieldMetadataType.TS_VECTOR: + case FieldMetadataType.RICH_TEXT: + return []; + + default: + return assertUnreachable(fieldMetadataItem.type); + } + }; + + const createBaseField = ( + fieldMetadataItem: FieldMetadataItem, + overrides: Partial = {}, + ): SpreadsheetImportField => { + return { + Icon: getIcon(fieldMetadataItem.icon), + label: fieldMetadataItem.label, + key: fieldMetadataItem.name, + fieldMetadataItemId: fieldMetadataItem.id, + fieldType: { type: 'input' }, + fieldMetadataType: fieldMetadataItem.type, + fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions( + fieldMetadataItem.type, + fieldMetadataItem.label, + ), + isNestedField: false, + ...overrides, + }; + }; + + const handleCompositeFields = ({ + fieldMetadataItem, + fieldType, + }: { + fieldMetadataItem: FieldMetadataItem; + fieldType: CompositeFieldType; + }) => { + const spreadsheetImportFields: SpreadsheetImportField[] = []; + + SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldType].subFields.forEach( + ({ subFieldName, isImportable, subFieldLabel }) => { + if (!isImportable) return; + const label = getCompositeSubFieldLabelWithFieldLabel( + fieldMetadataItem, + subFieldLabel, + ); + + spreadsheetImportFields.push( + createBaseField(fieldMetadataItem, { + label, + key: getCompositeSubFieldKey(fieldMetadataItem, subFieldName), + fieldValidationDefinitions: + getSpreadSheetFieldValidationDefinitions( + fieldMetadataItem.type, + label, + subFieldName, + ), + isNestedField: true, + isCompositeSubField: true, + compositeSubFieldKey: subFieldName, + }), + ); + }, + ); + + return spreadsheetImportFields; + }; + + const handleCompositeFieldFromRelationConnectField = ({ + fieldMetadataItem, + uniqueConstraintField, + uniqueConstraintType, + }: { + fieldMetadataItem: FieldMetadataItem; + uniqueConstraintField: FieldMetadataItem; + uniqueConstraintType: CompositeFieldType; + }) => { + const spreadsheetImportFields: SpreadsheetImportField[] = []; + + SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[ + uniqueConstraintType + ].subFields.forEach( + ({ subFieldName, isImportable, isIncludedInUniqueConstraint }) => { + if (!isImportable || !isIncludedInUniqueConstraint) return; + + spreadsheetImportFields.push( + createBaseField(fieldMetadataItem, { + label: getRelationConnectSubFieldLabel( + fieldMetadataItem, + uniqueConstraintField, + subFieldName, + ), + key: getRelationConnectSubFieldKey( + fieldMetadataItem, + uniqueConstraintField, + subFieldName, + ), + fieldValidationDefinitions: + getSpreadSheetFieldValidationDefinitions( + uniqueConstraintField.type, + uniqueConstraintField.name, + subFieldName, + ), + isNestedField: true, + isCompositeSubField: true, + compositeSubFieldKey: subFieldName, + uniqueFieldMetadataItem: uniqueConstraintField, + isRelationConnectField: true, + }), + ); + }, + ); + + return spreadsheetImportFields; + }; + + const handleSelectField = ( + fieldMetadataItem: FieldMetadataItem, + isMulti = false, + subFieldOverrides?: Record, + ) => + createBaseField(fieldMetadataItem, { + fieldType: { + type: isMulti ? 'multiSelect' : 'select', + options: + fieldMetadataItem.options?.map((option) => ({ + label: option.label, + value: option.value, + color: option.color, + })) || [], + ...(isDefined(subFieldOverrides) ? subFieldOverrides : {}), + }, + fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions( + fieldMetadataItem.type, + `${fieldMetadataItem.label} (ID)`, + ), + }); + + const handleRelationField = (fieldMetadataItem: FieldMetadataItem) => { + const spreadsheetImportFields: SpreadsheetImportField[] = []; + + const isManyToOneRelation = + fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE; + + const targetObjectMetadataItem = objectMetadataItems?.find( + (objectMetadataItem) => + objectMetadataItem.id === + fieldMetadataItem.relation?.targetObjectMetadata.id, + ); + + if (isManyToOneRelation && isDefined(targetObjectMetadataItem)) { + const uniqueConstraintFields = getUniqueConstraintsFields< + FieldMetadataItem, + ObjectMetadataItem + >(targetObjectMetadataItem); + + //todo - update logic when composite unique indexes will be supported + for (const uniqueConstraintField of uniqueConstraintFields.flat()) { + if (isCompositeFieldType(uniqueConstraintField.type)) { + spreadsheetImportFields.push( + ...handleCompositeFieldFromRelationConnectField({ + fieldMetadataItem, + uniqueConstraintField, + uniqueConstraintType: uniqueConstraintField.type, + }), + ); + } else { + spreadsheetImportFields.push( + ...buildSpreadsheetImportField(uniqueConstraintField, { + Icon: getIcon(fieldMetadataItem.icon), + isNestedField: true, + isCompositeSubField: false, + isRelationConnectField: true, + fieldMetadataItemId: fieldMetadataItem.id, + fieldMetadataType: FieldMetadataType.RELATION, + uniqueFieldMetadataItem: uniqueConstraintField, + label: getRelationConnectSubFieldLabel( + fieldMetadataItem, + uniqueConstraintField, + ), + key: getRelationConnectSubFieldKey( + fieldMetadataItem, + uniqueConstraintField, + ), + }), + ); + } + } + } + + return spreadsheetImportFields; + }; + + return { buildSpreadsheetImportFields }; +}; diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts index 6e255ba86..331eee1b4 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts @@ -1,8 +1,8 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useBatchCreateManyRecords } from '@/object-record/hooks/useBatchCreateManyRecords'; -import { useBuildAvailableFieldsForImport } from '@/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport'; +import { useBuildSpreadsheetImportFields } from '@/object-record/spreadsheet-import/hooks/useBuildSpreadSheetImportFields'; import { buildRecordFromImportedStructuredRow } from '@/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow'; -import { spreadsheetImportFilterAvailableFieldMetadataItems } from '@/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts'; +import { spreadsheetImportFilterAvailableFieldMetadataItems } from '@/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems'; import { spreadsheetImportGetUnicityRowHook } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetUnicityRowHook'; import { SpreadsheetImportCreateRecordsBatchSize } from '@/spreadsheet-import/constants/SpreadsheetImportCreateRecordsBatchSize'; import { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog'; @@ -10,12 +10,13 @@ import { spreadsheetImportCreatedRecordsProgressState } from '@/spreadsheet-impo import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSetRecoilState } from 'recoil'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; export const useOpenObjectRecordsSpreadsheetImportDialog = ( objectNameSingular: string, ) => { - const { openSpreadsheetImportDialog } = useOpenSpreadsheetImportDialog(); + const { openSpreadsheetImportDialog } = useOpenSpreadsheetImportDialog(); + const { buildSpreadsheetImportFields } = useBuildSpreadsheetImportFields(); + const { enqueueErrorSnackBar } = useSnackBar(); const { objectMetadataItem } = useObjectMetadataItem({ @@ -35,28 +36,19 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = ( abortController, }); - const { buildAvailableFieldsForImport } = useBuildAvailableFieldsForImport(); - const openObjectRecordsSpreadsheetImportDialog = ( options?: Omit< - SpreadsheetImportDialogOptions, + SpreadsheetImportDialogOptions, 'fields' | 'isOpen' | 'onClose' >, ) => { - //All fields that can be imported (included matchable and auto-filled) const availableFieldMetadataItemsToImport = spreadsheetImportFilterAvailableFieldMetadataItems( objectMetadataItem.fields, ); - const availableFieldMetadataItemsForMatching = - availableFieldMetadataItemsToImport.filter( - (fieldMetadataItem) => - fieldMetadataItem.type !== FieldMetadataType.ACTOR, - ); - - const availableFieldsForMatching = buildAvailableFieldsForImport( - availableFieldMetadataItemsForMatching, + const spreadsheetImportFields = buildSpreadsheetImportFields( + availableFieldMetadataItemsToImport, ); openSpreadsheetImportDialog({ @@ -66,7 +58,8 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = ( const fieldMapping: Record = buildRecordFromImportedStructuredRow({ importedStructuredRow: record, - fields: availableFieldMetadataItemsToImport, + fieldMetadataItems: availableFieldMetadataItemsToImport, + spreadsheetImportFields, }); return fieldMapping; @@ -83,7 +76,7 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = ( }); } }, - fields: availableFieldsForMatching, + spreadsheetImportFields, availableFieldMetadataItems: availableFieldMetadataItemsToImport, onAbortSubmit: () => { abortController.abort(); diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/types/AvailableFieldForImport.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/types/AvailableFieldForImport.ts deleted file mode 100644 index 1a8b46a62..000000000 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/types/AvailableFieldForImport.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { - SpreadsheetImportFieldType, - SpreadsheetImportFieldValidationDefinition, -} from '@/spreadsheet-import/types'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { IconComponent } from 'twenty-ui/display'; - -export type AvailableFieldForImport = { - Icon: IconComponent; - label: string; - key: string; - fieldType: SpreadsheetImportFieldType; - fieldValidationDefinitions?: SpreadsheetImportFieldValidationDefinition[]; - fieldMetadataType: FieldMetadataType; -}; 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 82099dd47..ed663b5fe 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 @@ -1,15 +1,20 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation'; import { buildRecordFromImportedStructuredRow } from '@/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow'; -import { ImportedStructuredRow } from '@/spreadsheet-import/types'; +import { + ImportedStructuredRow, + SpreadsheetImportField, +} from '@/spreadsheet-import/types'; import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { RelationType } from '~/generated/graphql'; describe('buildRecordFromImportedStructuredRow', () => { it('should successfully build a record from imported structured row', () => { - const importedStructuredRow: ImportedStructuredRow = { + const importedStructuredRow: ImportedStructuredRow = { booleanField: 'true', numberField: '30', multiSelectField: '["tag1", "tag2", "tag3"]', - relationField: 'company-123', + 'nameField (relationField)': 'John Doe', selectField: 'option1', arrayField: '["item1", "item2", "item3"]', jsonField: '{"key": "value", "nested": {"prop": "data"}}', @@ -122,6 +127,9 @@ describe('buildRecordFromImportedStructuredRow', () => { updatedAt: '2023-01-01', icon: 'IconBuilding', description: null, + relation: { + type: RelationType.MANY_TO_ONE, + } as FieldMetadataItemRelation, }, { id: '7', @@ -337,9 +345,25 @@ describe('buildRecordFromImportedStructuredRow', () => { }, ]; + 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, - fields, + fieldMetadataItems: fields, + spreadsheetImportFields, }); expect(result).toEqual({ @@ -350,7 +374,14 @@ describe('buildRecordFromImportedStructuredRow', () => { booleanField: true, numberField: 30, multiSelectField: ['tag1', 'tag2', 'tag3'], - relationFieldId: 'company-123', + relationField: { + connect: { + where: { + nameField: 'John Doe', + }, + }, + }, + relationFieldId: undefined, selectField: 'option1', arrayField: ['item1', 'item2', 'item3'], jsonField: { key: 'value', nested: { prop: 'data' } }, @@ -406,8 +437,8 @@ describe('buildRecordFromImportedStructuredRow', () => { }); }); - it('should handle case where user provides only a primaryPhoneNumber without calling code', () => { - const importedStructuredRow: ImportedStructuredRow = { + it('should successfully build a record from imported structured row with primary phone number (without calling code)', () => { + const importedStructuredRow: ImportedStructuredRow = { 'Primary Phone Number (phoneField)': '5550123', }; @@ -430,7 +461,8 @@ describe('buildRecordFromImportedStructuredRow', () => { const result = buildRecordFromImportedStructuredRow({ importedStructuredRow, - fields, + fieldMetadataItems: fields, + spreadsheetImportFields: [], }); expect(result).toEqual({ @@ -440,4 +472,64 @@ describe('buildRecordFromImportedStructuredRow', () => { }, }); }); + + it('should successfully build a record from imported structured row with relation composite subfield', () => { + const importedStructuredRow: ImportedStructuredRow = { + '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', + isNestedField: false, + isRelationConnectField: true, + label: 'Relation Field / Email Field', + key: 'emailField (relationField)', + fieldMetadataType: FieldMetadataType.RELATION, + uniqueFieldMetadataItem: { + name: 'emailField', + type: FieldMetadataType.EMAILS, + }, + compositeSubFieldKey: 'primaryEmail', + }, + ] as SpreadsheetImportField[]; + + const result = buildRecordFromImportedStructuredRow({ + importedStructuredRow, + fieldMetadataItems: fields, + spreadsheetImportFields, + }); + + expect(result).toEqual({ + relationField: { + connect: { + where: { + emailField: { + primaryEmail: 'john.doe@example.com', + }, + }, + }, + }, + }); + }); }); diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/__tests__/spreadsheetImportGetUnicityRowHook.test.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/__tests__/spreadsheetImportGetUnicityRowHook.test.ts index 3575d398a..498602b37 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/__tests__/spreadsheetImportGetUnicityRowHook.test.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/__tests__/spreadsheetImportGetUnicityRowHook.test.ts @@ -79,7 +79,7 @@ describe('spreadsheetImportGetUnicityRowHook', () => { it('should return row with error if row is not unique - index on composite field', () => { const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem); - const testData: ImportedStructuredRow[] = [ + const testData: ImportedStructuredRow[] = [ { 'Link URL (domainName)': 'https://duplicaTe.com' }, { 'Link URL (domainName)': 'https://duplicate.com' }, { 'Link URL (domainName)': 'https://other.com' }, @@ -100,7 +100,7 @@ describe('spreadsheetImportGetUnicityRowHook', () => { it('should return row with error if row is not unique - index on id', () => { const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem); - const testData: ImportedStructuredRow[] = [ + const testData: ImportedStructuredRow[] = [ { 'Link URL (domainName)': 'test.com', id: '1' }, { 'Link URL (domainName)': 'test2.com', id: '1' }, { 'Link URL (domainName)': 'test3.com', id: '3' }, @@ -120,7 +120,7 @@ describe('spreadsheetImportGetUnicityRowHook', () => { it('should return row with error if row is not unique - multi fields index', () => { const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem); - const testData: ImportedStructuredRow[] = [ + const testData: ImportedStructuredRow[] = [ { name: 'test', employees: '100', id: '1' }, { name: 'test', employees: '100', id: '2' }, { name: 'test', employees: '101', id: '3' }, @@ -143,7 +143,7 @@ describe('spreadsheetImportGetUnicityRowHook', () => { it('should not add error if row values are unique', () => { const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem); - const testData: ImportedStructuredRow[] = [ + const testData: ImportedStructuredRow[] = [ { name: 'test', 'Link URL (domainName)': 'test.com', 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 6d5a160fc..604b88f98 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 @@ -1,31 +1,38 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; import { FieldActorForInputValue } from '@/object-record/record-field/types/FieldMetadata'; -import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey'; -import { ImportedStructuredRow } from '@/spreadsheet-import/types'; +import { getCompositeSubFieldKey } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldKey'; +import { + ImportedStructuredRow, + SpreadsheetImportFields, +} from '@/spreadsheet-import/types'; import { isNonEmptyString } from '@sniptt/guards'; import { CountryCode, parsePhoneNumberWithError } from 'libphonenumber-js'; -import { isDefined } from 'twenty-shared/utils'; +import { assertUnreachable, isDefined } from 'twenty-shared/utils'; import { z } from 'zod'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql'; import { castToString } from '~/utils/castToString'; import { convertCurrencyAmountToCurrencyMicros } from '~/utils/convertCurrencyToCurrencyMicros'; import { isEmptyObject } from '~/utils/isEmptyObject'; import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString'; type BuildRecordFromImportedStructuredRowArgs = { - importedStructuredRow: ImportedStructuredRow; - fields: FieldMetadataItem[]; + importedStructuredRow: ImportedStructuredRow; + fieldMetadataItems: FieldMetadataItem[]; + spreadsheetImportFields: SpreadsheetImportFields; }; const buildCompositeFieldRecord = ( field: FieldMetadataItem, - importedStructuredRow: ImportedStructuredRow, + importedStructuredRow: ImportedStructuredRow, compositeFieldConfig: Record any) | undefined>, ): Record | undefined => { const compositeFieldRecord = Object.entries(compositeFieldConfig).reduce( (acc, [compositeFieldKey, transform]) => { const value = - importedStructuredRow[getSubFieldOptionKey(field, compositeFieldKey)]; + importedStructuredRow[ + getCompositeSubFieldKey(field, compositeFieldKey) + ]; return isDefined(value) ? { ...acc, [compositeFieldKey]: transform?.(value) || value } @@ -37,9 +44,59 @@ const buildCompositeFieldRecord = ( return isEmptyObject(compositeFieldRecord) ? undefined : compositeFieldRecord; }; +const buildRelationConnectFieldRecord = ( + fieldMetadataItem: FieldMetadataItem, + importedStructuredRow: ImportedStructuredRow, + spreadsheetImportFields: SpreadsheetImportFields, +) => { + if (fieldMetadataItem.relation?.type !== RelationType.MANY_TO_ONE) + return undefined; + + const relationConnectFields = spreadsheetImportFields.filter( + (field) => + field.fieldMetadataItemId === fieldMetadataItem.id && + isDefined(importedStructuredRow[field.key]) && + isNonEmptyString(importedStructuredRow[field.key]), + ); + + if (relationConnectFields.length === 0) return undefined; + + const relationConnectFieldValue = relationConnectFields.reduce( + (acc, field) => { + const uniqueFieldMetadataItem = field.uniqueFieldMetadataItem; + if (!isDefined(uniqueFieldMetadataItem)) return acc; + + if ( + isCompositeFieldType(uniqueFieldMetadataItem.type) && + isDefined(field.compositeSubFieldKey) + ) { + return { + ...acc, + [uniqueFieldMetadataItem.name]: { + ...(isDefined(acc?.[uniqueFieldMetadataItem.name]) + ? acc[uniqueFieldMetadataItem.name] + : {}), + [field.compositeSubFieldKey]: importedStructuredRow[field.key], + }, + }; + } + return { + ...acc, + [uniqueFieldMetadataItem.name]: importedStructuredRow[field.key], + }; + }, + {} as Record, + ); + + return isEmptyObject(relationConnectFieldValue) + ? undefined + : { connect: { where: relationConnectFieldValue } }; +}; + export const buildRecordFromImportedStructuredRow = ({ - fields, + fieldMetadataItems, importedStructuredRow, + spreadsheetImportFields, }: BuildRecordFromImportedStructuredRowArgs) => { const stringArrayJSONSchema = z .preprocess((value) => { @@ -145,7 +202,7 @@ export const buildRecordFromImportedStructuredRow = ({ }, }; - for (const field of fields) { + for (const field of fieldMetadataItems) { const importedFieldValue = importedStructuredRow[field.name]; switch (field.type) { @@ -178,12 +235,12 @@ export const buildRecordFromImportedStructuredRow = ({ const primaryPhoneNumber = importedStructuredRow[ - getSubFieldOptionKey(field, 'primaryPhoneNumber') + getCompositeSubFieldKey(field, 'primaryPhoneNumber') ]; const primaryPhoneCallingCode = importedStructuredRow[ - getSubFieldOptionKey(field, 'primaryPhoneCallingCode') + getCompositeSubFieldKey(field, 'primaryPhoneCallingCode') ]; const hasUserProvidedPrimaryPhoneNumberWithoutCallingCode = @@ -195,7 +252,7 @@ export const buildRecordFromImportedStructuredRow = ({ if (hasUserProvidedPrimaryPhoneNumberWithoutCallingCode) { const primaryPhoneCountryCode = importedStructuredRow[ - getSubFieldOptionKey(field, 'primaryPhoneCountryCode') + getCompositeSubFieldKey(field, 'primaryPhoneCountryCode') ]; const hasUserProvidedPrimaryPhoneCountryCode = @@ -237,22 +294,14 @@ export const buildRecordFromImportedStructuredRow = ({ case FieldMetadataType.NUMERIC: recordToBuild[field.name] = Number(importedFieldValue); break; - case FieldMetadataType.UUID: - if ( - isDefined(importedFieldValue) && - isNonEmptyString(importedFieldValue) - ) { - recordToBuild[field.name] = importedFieldValue; - } - break; - case FieldMetadataType.RELATION: - if ( - isDefined(importedFieldValue) && - isNonEmptyString(importedFieldValue) - ) - recordToBuild[field.name + 'Id'] = importedFieldValue; - + case FieldMetadataType.RELATION: { + recordToBuild[field.name] = buildRelationConnectFieldRecord( + field, + importedStructuredRow, + spreadsheetImportFields, + ); break; + } case FieldMetadataType.ACTOR: recordToBuild[field.name] = { source: 'IMPORT', @@ -275,11 +324,30 @@ export const buildRecordFromImportedStructuredRow = ({ } break; } - default: + case FieldMetadataType.UUID: + case FieldMetadataType.DATE: + case FieldMetadataType.DATE_TIME: + if ( + isDefined(importedFieldValue) && + isNonEmptyString(importedFieldValue) + ) { + recordToBuild[field.name] = importedFieldValue; + } + break; + case FieldMetadataType.SELECT: + case FieldMetadataType.RATING: + case FieldMetadataType.TEXT: if (isDefined(importedFieldValue)) { recordToBuild[field.name] = importedFieldValue; } break; + case FieldMetadataType.MORPH_RELATION: + case FieldMetadataType.POSITION: + case FieldMetadataType.RICH_TEXT: + case FieldMetadataType.TS_VECTOR: + break; + default: + assertUnreachable(field.type); } } diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadSheetGetRelationConnectSubFieldKey.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadSheetGetRelationConnectSubFieldKey.ts new file mode 100644 index 000000000..369328d7d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadSheetGetRelationConnectSubFieldKey.ts @@ -0,0 +1,10 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { isDefined } from 'twenty-shared/utils'; + +export const getRelationConnectSubFieldKey = ( + fieldMetadataItem: FieldMetadataItem, + uniqueConstraintField: FieldMetadataItem, + compositeSubFieldKey?: string, +) => { + return `${isDefined(compositeSubFieldKey) ? `${compositeSubFieldKey}-${uniqueConstraintField.name}` : uniqueConstraintField.name} (${fieldMetadataItem.name})`; +}; diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts.ts rename to packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSubFieldOptionKey.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldKey.ts similarity index 69% rename from packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSubFieldOptionKey.ts rename to packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldKey.ts index b9f0d78d5..9cfe6df6a 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSubFieldOptionKey.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldKey.ts @@ -2,20 +2,18 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; import { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constants/CompositeFieldSubFieldLabel'; -export const getSubFieldOptionKey = ( +export const getCompositeSubFieldKey = ( fieldMetadataItem: FieldMetadataItem, subFieldName: string, ) => { if (!isCompositeFieldType(fieldMetadataItem.type)) { throw new Error( - `getSubFieldOptionKey can only be called for composite field types. Received: ${fieldMetadataItem.type}`, + `getCompositeSubFieldKey can only be called for composite field types. Received: ${fieldMetadataItem.type}`, ); } const subFieldLabel = COMPOSITE_FIELD_SUB_FIELD_LABELS[fieldMetadataItem.type][subFieldName]; - const subFieldKey = `${subFieldLabel} (${fieldMetadataItem.name})`; - - return subFieldKey; + return `${subFieldLabel} (${fieldMetadataItem.name})`; }; diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldLabelWithFieldLabel.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldLabelWithFieldLabel.ts new file mode 100644 index 000000000..da5ae43ed --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldLabelWithFieldLabel.ts @@ -0,0 +1,8 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; + +export const getCompositeSubFieldLabelWithFieldLabel = ( + fieldMetadataItem: FieldMetadataItem, + subFieldLabel: string, +) => { + return `${fieldMetadataItem.label} / ${subFieldLabel}`; +}; diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetRelationConnectSubFieldLabel.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetRelationConnectSubFieldLabel.ts new file mode 100644 index 000000000..cd75f066b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetRelationConnectSubFieldLabel.ts @@ -0,0 +1,20 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; +import { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constants/CompositeFieldSubFieldLabel'; +import { isDefined } from 'twenty-shared/utils'; + +export const getRelationConnectSubFieldLabel = ( + fieldMetadataItem: FieldMetadataItem, + uniqueFieldMetadataItem: FieldMetadataItem, + compositeSubFieldKey?: string, +) => { + const compositeSubFieldLabel = + isCompositeFieldType(fieldMetadataItem.type) && + isDefined(compositeSubFieldKey) + ? COMPOSITE_FIELD_SUB_FIELD_LABELS[fieldMetadataItem.type][ + compositeSubFieldKey + ] + : undefined; + + return `${fieldMetadataItem.label} / ${uniqueFieldMetadataItem.label}${compositeSubFieldLabel ? ` / ${compositeSubFieldLabel}` : ''}`; +}; diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetUnicityRowHook.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetUnicityRowHook.ts index 52343e47d..8d17f2eca 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetUnicityRowHook.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetUnicityRowHook.ts @@ -1,6 +1,7 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; -import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey'; +import { getCompositeSubFieldKey } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldKey'; import { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constants/CompositeFieldSubFieldLabel'; import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; import { @@ -11,6 +12,7 @@ import { t } from '@lingui/core/macro'; import { isNonEmptyString } from '@sniptt/guards'; import { FieldMetadataType } from 'twenty-shared/types'; import { + getUniqueConstraintsFields, isDefined, lowercaseUrlOriginAndRemoveTrailingSlash, } from 'twenty-shared/utils'; @@ -23,22 +25,14 @@ type Column = { export const spreadsheetImportGetUnicityRowHook = ( objectMetadataItem: ObjectMetadataItem, ) => { - const uniqueConstraints = objectMetadataItem.indexMetadatas.filter( - (indexMetadata) => indexMetadata.isUnique, - ); - - const uniqueConstraintsWithColumnNames: Column[][] = [ - [{ columnName: 'id', fieldType: FieldMetadataType.UUID }], - ...uniqueConstraints.map((indexMetadata) => - indexMetadata.indexFieldMetadatas.flatMap((indexField) => { - const field = objectMetadataItem.fields.find( - (objectField) => objectField.id === indexField.fieldMetadataId, - ); - - if (!field) { - return []; - } + const uniqueConstraintsFields = getUniqueConstraintsFields< + FieldMetadataItem, + ObjectMetadataItem + >(objectMetadataItem); + const uniqueConstraintsWithColumnNames: Column[][] = + uniqueConstraintsFields.map((uniqueConstraintFields) => + uniqueConstraintFields.flatMap((field) => { if (isCompositeFieldType(field.type)) { const compositeTypeFieldConfig = SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[field.type]; @@ -48,18 +42,16 @@ export const spreadsheetImportGetUnicityRowHook = ( ); return uniqueSubFields.map((subField) => ({ - columnName: getSubFieldOptionKey(field, subField.subFieldName), + columnName: getCompositeSubFieldKey(field, subField.subFieldName), fieldType: field.type, })); } return [{ columnName: field.name, fieldType: field.type }]; }), - ), - ]; - - const rowHook: SpreadsheetImportRowHook = (row, addError, table) => { - if (uniqueConstraints.length === 0) { + ); + const rowHook: SpreadsheetImportRowHook = (row, addError, table) => { + if (uniqueConstraintsFields.length === 0) { return row; } @@ -95,7 +87,7 @@ export const spreadsheetImportGetUnicityRowHook = ( }; const getUniqueValues = ( - row: ImportedStructuredRow, + row: ImportedStructuredRow, uniqueConstraint: Column[], ) => { return uniqueConstraint 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 7cfd395c2..f42ed7033 100644 --- a/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts +++ b/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts @@ -42,18 +42,10 @@ export const sanitizeRecordInput = ({ if ( isDefined(fieldMetadataItem) && fieldMetadataItem.type === FieldMetadataType.RELATION && - fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE + fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE && + !isDefined(recordInput[fieldMetadataItem.name]?.connect?.where) ) { - const relationIdFieldName = `${fieldMetadataItem.name}Id`; - const relationIdFieldMetadataItem = objectMetadataItem.fields.find( - (field) => field.name === relationIdFieldName, - ); - - const relationIdFieldValue = recordInput[relationIdFieldName]; - - return relationIdFieldMetadataItem - ? [relationIdFieldName, relationIdFieldValue ?? null] - : undefined; + return undefined; } if ( diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts index 44852d244..b7a489afd 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts @@ -27,7 +27,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; //TODO : isIncludedInUniqueConstraint refactor - https://github.com/twentyhq/core-team-issues/issues/1097 -type CompositeSubFieldConfig = { +export type CompositeSubFieldConfig = { subFieldName: keyof T; subFieldLabel: string; isImportable: boolean; @@ -258,7 +258,7 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = { .firstName, isImportable: true, isFilterable: true, - isIncludedInUniqueConstraint: true, + isIncludedInUniqueConstraint: false, }, { subFieldName: 'lastName', @@ -267,7 +267,7 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = { .lastName, isImportable: true, isFilterable: true, - isIncludedInUniqueConstraint: true, + isIncludedInUniqueConstraint: false, }, ], exampleValues: [ diff --git a/packages/twenty-front/src/modules/spreadsheet-import/__mocks__/mockRsiValues.ts b/packages/twenty-front/src/modules/spreadsheet-import/__mocks__/mockRsiValues.ts index 3500e7437..346942c28 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/__mocks__/mockRsiValues.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/__mocks__/mockRsiValues.ts @@ -1,7 +1,7 @@ import { defaultSpreadsheetImportProps } from '@/spreadsheet-import/provider/components/SpreadsheetImport'; import { - SpreadsheetImportDialogOptions, - SpreadsheetImportFields + SpreadsheetImportDialogOptions, + SpreadsheetImportFields } from '@/spreadsheet-import/types'; import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns'; import { FieldMetadataType } from 'twenty-shared/types'; @@ -16,7 +16,6 @@ const fields = [ fieldType: { type: 'input', }, - example: 'Stephanie', fieldValidationDefinitions: [ { rule: 'required', @@ -24,6 +23,8 @@ const fields = [ }, ], fieldMetadataType: FieldMetadataType.TEXT, + fieldMetadataItemId: '1', + isNestedField: false, }, { Icon: null, @@ -33,7 +34,6 @@ const fields = [ fieldType: { type: 'input', }, - example: 'McDonald', fieldValidationDefinitions: [ { rule: 'unique', @@ -42,6 +42,9 @@ const fields = [ }, ], description: 'Family / Last name', + fieldMetadataType: FieldMetadataType.TEXT, + fieldMetadataItemId: '2', + isNestedField: false, }, { Icon: null, @@ -51,7 +54,6 @@ const fields = [ fieldType: { type: 'input', }, - example: '23', fieldValidationDefinitions: [ { rule: 'regex', @@ -60,12 +62,14 @@ const fields = [ level: 'warning', }, ], + fieldMetadataType: FieldMetadataType.TEXT, + fieldMetadataItemId: '3', + isNestedField: false, }, { Icon: null, label: 'Team', key: 'team', - alternateMatches: ['department'], fieldType: { type: 'select', options: [ @@ -73,28 +77,31 @@ const fields = [ { label: 'Team Two', value: 'two' }, ], }, - example: 'Team one', fieldValidationDefinitions: [ { rule: 'required', errorMessage: 'Team is required', }, ], + fieldMetadataType: FieldMetadataType.TEXT, + fieldMetadataItemId: '4', + isNestedField: false, }, { Icon: null, label: 'Is manager', key: 'is_manager', - alternateMatches: ['manages'], fieldType: { type: 'checkbox', booleanMatches: {}, }, - example: 'true', + fieldMetadataType: FieldMetadataType.TEXT, + fieldMetadataItemId: '5', + isNestedField: false, }, -] as SpreadsheetImportFields; +] as SpreadsheetImportFields; -export const importedColums: SpreadsheetColumns = [ +export const importedColums: SpreadsheetColumns = [ { header: 'Name', index: 0, @@ -121,13 +128,13 @@ export const importedColums: SpreadsheetColumns = [ }, ]; -const mockComponentBehaviourForTypes = ( - props: SpreadsheetImportDialogOptions, +const mockComponentBehaviourForTypes = ( + props: SpreadsheetImportDialogOptions, ) => props; export const mockRsiValues = mockComponentBehaviourForTypes({ ...defaultSpreadsheetImportProps, - fields: fields, + spreadsheetImportFields: fields, onSubmit: async () => { return; }, diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent.tsx index 20fec1153..5538f797b 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent.tsx @@ -1,8 +1,8 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { getFieldMetadataTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFieldMetadataTypeLabel'; -import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; import { DO_NOT_IMPORT_OPTION_KEY } from '@/spreadsheet-import/constants/DoNotImportOptionKey'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; +import { hasNestedFields } from '@/spreadsheet-import/utils/spreadsheetImportHasNestedFields'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; @@ -139,7 +139,7 @@ export const MatchColumnSelectFieldSelectDropdownContent = ({ LeftIcon={getIcon(field.icon)} text={field.label} contextualText={getFieldMetadataTypeLabel(field.type)} - hasSubMenu={isCompositeFieldType(field.type)} + hasSubMenu={hasNestedFields(field)} /> ))} diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx index e63c0d792..968a963c2 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx @@ -1,9 +1,7 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel'; -import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; -import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey'; -import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; -import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType'; +import { SpreadsheetImportFieldOption } from '@/spreadsheet-import/types/SpreadsheetImportFieldOption'; +import { getSubFieldOptions } from '@/spreadsheet-import/utils/spreadsheetImportGetSubFieldOptions'; +import { hasNestedFields } from '@/spreadsheet-import/utils/spreadsheetImportHasNestedFields'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; @@ -12,15 +10,8 @@ import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/Dropdow import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; import { useState } from 'react'; -import { isDefined } from 'twenty-shared/utils'; -import { - IconChevronLeft, - OverflowingTextWithTooltip, - useIcons, -} from 'twenty-ui/display'; -import { SelectOption } from 'twenty-ui/input'; +import { IconChevronLeft, OverflowingTextWithTooltip } from 'twenty-ui/display'; import { MenuItem } from 'twenty-ui/navigation'; -import { ReadonlyDeep } from 'type-fest'; export const MatchColumnSelectSubFieldSelectDropdownContent = ({ fieldMetadataItem, @@ -30,13 +21,11 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({ }: { fieldMetadataItem: FieldMetadataItem; onSubFieldSelect: (subFieldNameSelected: string) => void; - options: readonly ReadonlyDeep[]; + options: readonly Readonly[]; onBack: () => void; }) => { const [searchFilter, setSearchFilter] = useState(''); - const { getIcon } = useIcons(); - const handleFilterChange = (event: React.ChangeEvent) => { const value = event.currentTarget.value; @@ -52,31 +41,15 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({ onBack(); }; - if (!isCompositeFieldType(fieldMetadataItem.type)) { + if (!hasNestedFields(fieldMetadataItem)) { return <>; } - const fieldMetadataItemSettings = - SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldMetadataItem.type]; - - const subFieldsThatExistInOptions = fieldMetadataItemSettings.subFields - .filter(({ subFieldName }) => { - const optionKey = getSubFieldOptionKey(fieldMetadataItem, subFieldName); - - const correspondingOption = options.find( - (option) => option.value === optionKey, - ); - - return isDefined(correspondingOption); - }) - .filter(({ subFieldName }) => - getCompositeSubFieldLabel( - fieldMetadataItem.type as CompositeFieldType, - subFieldName, - ) - .toLowerCase() - .includes(searchFilter.toLowerCase()), - ); + const subFieldOptions = getSubFieldOptions( + fieldMetadataItem, + options, + searchFilter, + ); return ( @@ -97,24 +70,17 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({ /> - {subFieldsThatExistInOptions.map(({ subFieldName }) => ( - handleSubFieldSelect(subFieldName)} - LeftIcon={getIcon(fieldMetadataItem.icon)} - text={getCompositeSubFieldLabel( - fieldMetadataItem.type as CompositeFieldType, - subFieldName, - )} - disabled={ - options.find( - (option) => - option.value === - getSubFieldOptionKey(fieldMetadataItem, subFieldName), - )?.disabled - } - /> - ))} + {subFieldOptions.map( + ({ value, shortLabelForNestedField, Icon, disabled }) => ( + handleSubFieldSelect(value)} + LeftIcon={Icon} + text={shortLabelForNestedField} + disabled={disabled} + /> + ), + )} ); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx index 32a8bdcae..649cd29aa 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx @@ -3,10 +3,11 @@ import { ReadonlyDeep } from 'type-fest'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; -import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey'; import { MatchColumnSelectFieldSelectDropdownContent } from '@/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent'; import { MatchColumnSelectSubFieldSelectDropdownContent } from '@/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent'; import { DO_NOT_IMPORT_OPTION_KEY } from '@/spreadsheet-import/constants/DoNotImportOptionKey'; +import { SpreadsheetImportFieldOption } from '@/spreadsheet-import/types/SpreadsheetImportFieldOption'; +import { hasNestedFields } from '@/spreadsheet-import/utils/spreadsheetImportHasNestedFields'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; import styled from '@emotion/styled'; @@ -19,7 +20,7 @@ interface MatchColumnToFieldSelectProps { columnIndex: string; onChange: (value: ReadonlyDeep | null) => void; value?: ReadonlyDeep; - options: readonly ReadonlyDeep[]; + options: readonly Readonly[]; suggestedOptions: readonly ReadonlyDeep[]; placeholder?: string; } @@ -70,12 +71,7 @@ export const MatchColumnToFieldSelect = ({ } const correspondingOption = options.find((option) => { - const optionKey = getSubFieldOptionKey( - selectedFieldMetadataItem, - subFieldNameSelected, - ); - - return option.value === optionKey; + return option.value === subFieldNameSelected; }); if (isDefined(correspondingOption)) { @@ -112,9 +108,9 @@ export const MatchColumnToFieldSelect = ({ closeDropdown(dropdownId); }; - const shouldShowSubField = + const shouldShowNestedField = isDefined(selectedFieldMetadataItem) && - isCompositeFieldType(selectedFieldMetadataItem.type); + hasNestedFields(selectedFieldMetadataItem); return ( } dropdownComponents={ - shouldShowSubField ? ( + shouldShowNestedField ? ( = { +type ReactSpreadsheetImportContextProviderProps = { children: React.ReactNode; - values: SpreadsheetImportDialogOptions; + values: SpreadsheetImportDialogOptions; }; -export const ReactSpreadsheetImportContextProvider = ({ +export const ReactSpreadsheetImportContextProvider = ({ children, values, -}: ReactSpreadsheetImportContextProviderProps) => { - if (isUndefinedOrNull(values.fields)) { +}: ReactSpreadsheetImportContextProviderProps) => { + if (isUndefinedOrNull(values.spreadsheetImportFields)) { throw new Error('Fields must be provided to spreadsheet-import'); } diff --git a/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx b/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx index e48fe1b38..69f0495b6 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx @@ -13,45 +13,43 @@ import { act } from 'react'; const Wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); -type SpreadsheetKey = 'spreadsheet_key'; -export const mockedSpreadsheetOptions: SpreadsheetImportDialogOptions = - { - onClose: () => {}, - fields: [], - uploadStepHook: async () => [], - selectHeaderStepHook: async ( - headerValues: ImportedRow, - data: ImportedRow[], - ) => ({ - headerRow: headerValues, - importedRows: data, - }), - matchColumnsStepHook: async () => [], - rowHook: () => ({ spreadsheet_key: 'rowHook' }), - tableHook: () => [{ spreadsheet_key: 'tableHook' }], - onSubmit: async () => {}, - allowInvalidSubmit: false, - customTheme: {}, - maxRecords: 10, - maxFileSize: 50, - autoMapHeaders: true, - autoMapDistance: 1, - initialStepState: { - type: SpreadsheetImportStepType.upload, - }, - dateFormat: 'MM/DD/YY', - parseRaw: true, - rtl: false, - selectHeader: true, - availableFieldMetadataItems: [], - }; +export const mockedSpreadsheetOptions: SpreadsheetImportDialogOptions = { + onClose: () => {}, + spreadsheetImportFields: [], + uploadStepHook: async () => [], + selectHeaderStepHook: async ( + headerValues: ImportedRow, + data: ImportedRow[], + ) => ({ + headerRow: headerValues, + importedRows: data, + }), + matchColumnsStepHook: async () => [], + rowHook: () => ({ spreadsheet_key: 'rowHook' }), + tableHook: () => [{ spreadsheet_key: 'tableHook' }], + onSubmit: async () => {}, + allowInvalidSubmit: false, + customTheme: {}, + maxRecords: 10, + maxFileSize: 50, + autoMapHeaders: true, + autoMapDistance: 1, + initialStepState: { + type: SpreadsheetImportStepType.upload, + }, + dateFormat: 'MM/DD/YY', + parseRaw: true, + rtl: false, + selectHeader: true, + availableFieldMetadataItems: [], +}; describe('useSpreadsheetImport', () => { it('should set isOpen to true, and update the options in the Recoil state', async () => { const { result } = renderHook( () => ({ - useSpreadsheetImport: useOpenSpreadsheetImportDialog(), + useSpreadsheetImport: useOpenSpreadsheetImportDialog(), spreadsheetImportState: useRecoilState(spreadsheetImportDialogState)[0], }), { diff --git a/packages/twenty-front/src/modules/spreadsheet-import/hooks/useComputeColumnSuggestionsAndAutoMatch.ts b/packages/twenty-front/src/modules/spreadsheet-import/hooks/useComputeColumnSuggestionsAndAutoMatch.ts index 5640640da..7300a0a60 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/hooks/useComputeColumnSuggestionsAndAutoMatch.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/hooks/useComputeColumnSuggestionsAndAutoMatch.ts @@ -8,8 +8,9 @@ import { ImportedRow } from '@/spreadsheet-import/types'; import { getMatchedColumnsWithFuse } from '@/spreadsheet-import/utils/getMatchedColumnsWithFuse'; import { useRecoilCallback } from 'recoil'; -export const useComputeColumnSuggestionsAndAutoMatch = () => { - const { fields, autoMapHeaders } = useSpreadsheetImportInternal(); +export const useComputeColumnSuggestionsAndAutoMatch = () => { + const { spreadsheetImportFields: fields, autoMapHeaders } = + useSpreadsheetImportInternal(); const computeColumnSuggestionsAndAutoMatch = useRecoilCallback( ({ set, snapshot }) => diff --git a/packages/twenty-front/src/modules/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog.ts b/packages/twenty-front/src/modules/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog.ts index f68cc75cd..3e1d06497 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog.ts @@ -4,13 +4,13 @@ import { SPREADSHEET_IMPORT_MODAL_ID } from '@/spreadsheet-import/constants/Spre import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState'; import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types'; import { useModal } from '@/ui/layout/modal/hooks/useModal'; -export const useOpenSpreadsheetImportDialog = () => { +export const useOpenSpreadsheetImportDialog = () => { const setSpreadSheetImport = useSetRecoilState(spreadsheetImportDialogState); const { openModal } = useModal(); const openSpreadsheetImportDialog = ( - options: Omit, 'isOpen' | 'onClose'>, + options: Omit, ) => { openModal(SPREADSHEET_IMPORT_MODAL_ID); setSpreadSheetImport({ diff --git a/packages/twenty-front/src/modules/spreadsheet-import/hooks/useSpreadsheetImportInternal.ts b/packages/twenty-front/src/modules/spreadsheet-import/hooks/useSpreadsheetImportInternal.ts index fd5aec6c3..85ef96029 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/hooks/useSpreadsheetImportInternal.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/hooks/useSpreadsheetImportInternal.ts @@ -5,10 +5,10 @@ import { RsiContext } from '@/spreadsheet-import/components/ReactSpreadsheetImpo import { defaultSpreadsheetImportProps } from '@/spreadsheet-import/provider/components/SpreadsheetImport'; import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types'; -export const useSpreadsheetImportInternal = () => +export const useSpreadsheetImportInternal = () => useContext< SetRequired< - SpreadsheetImportDialogOptions, + SpreadsheetImportDialogOptions, keyof typeof defaultSpreadsheetImportProps > >(RsiContext); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImport.tsx b/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImport.tsx index ef265a29e..a94169eec 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImport.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImport.tsx @@ -10,9 +10,7 @@ import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogMa import { useStepBar } from '@/ui/navigation/step-bar/hooks/useStepBar'; import { useLingui } from '@lingui/react/macro'; -export const defaultSpreadsheetImportProps: Partial< - SpreadsheetImportProps -> = { +export const defaultSpreadsheetImportProps: Partial = { autoMapHeaders: true, allowInvalidSubmit: true, autoMapDistance: 2, @@ -28,13 +26,11 @@ export const defaultSpreadsheetImportProps: Partial< maxRecords: SpreadsheetMaxRecordImportCapacity, } as const; -export const SpreadsheetImport = ( - props: SpreadsheetImportProps, -) => { +export const SpreadsheetImport = (props: SpreadsheetImportProps) => { const mergedProps = { ...defaultSpreadsheetImportProps, ...props, - } as SpreadsheetImportProps; + } as SpreadsheetImportProps; const { enqueueDialog } = useDialogManager(); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/states/spreadsheetImportDialogState.ts b/packages/twenty-front/src/modules/spreadsheet-import/states/spreadsheetImportDialogState.ts index cd0d5c904..cfec8c88a 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/states/spreadsheetImportDialogState.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/states/spreadsheetImportDialogState.ts @@ -1,19 +1,18 @@ import { createState } from 'twenty-ui/utilities'; import { SpreadsheetImportDialogOptions } from '../types'; -export type SpreadsheetImportDialogState = { +export type SpreadsheetImportDialogState = { isOpen: boolean; isStepBarVisible: boolean; - options: Omit, 'isOpen' | 'onClose'> | null; + options: Omit | null; }; -export const spreadsheetImportDialogState = createState< - SpreadsheetImportDialogState ->({ - key: 'spreadsheetImportDialogState', - defaultValue: { - isOpen: false, - isStepBarVisible: true, - options: null, - }, -}); +export const spreadsheetImportDialogState = + createState({ + key: 'spreadsheetImportDialogState', + defaultValue: { + isOpen: false, + isStepBarVisible: true, + options: null, + }, + }); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx index 7ba45e779..bbcfc4b09 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx @@ -1,7 +1,6 @@ import { useRecoilValue } from 'recoil'; import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton'; -import { useHideStepBar } from '@/spreadsheet-import/hooks/useHideStepBar'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { spreadsheetImportCreatedRecordsProgressState } from '@/spreadsheet-import/states/spreadsheetImportCreatedRecordsProgressState'; import { Modal } from '@/ui/layout/modal/components/Modal'; @@ -38,9 +37,6 @@ type ImportDataStepProps = { export const ImportDataStep = ({ recordsToImportCount, }: ImportDataStepProps) => { - const hideStepBar = useHideStepBar(); - hideStepBar(); - const { onClose } = useSpreadsheetImportInternal(); const spreadsheetImportCreatedRecordsProgress = useRecoilValue( spreadsheetImportCreatedRecordsProgressState, diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx index 3054a4308..f4bf87b71 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx @@ -64,7 +64,7 @@ export type MatchColumnsStepProps = { onError: (message: string) => void; }; -export const MatchColumnsStep = ({ +export const MatchColumnsStep = ({ data, headerValues, onBack, @@ -76,7 +76,7 @@ export const MatchColumnsStep = ({ }: MatchColumnsStepProps) => { const { enqueueDialog } = useDialogManager(); const dataExample = data.slice(0, 2); - const { fields } = useSpreadsheetImportInternal(); + const { spreadsheetImportFields: fields } = useSpreadsheetImportInternal(); const [isLoading, setIsLoading] = useState(false); const [columns, setColumns] = useRecoilState( initialComputedColumnsSelector(headerValues), @@ -90,7 +90,7 @@ export const MatchColumnsStep = ({ (columnIndex: number) => { setColumns( columns.map((column, index) => - columnIndex === index ? setIgnoreColumn(column) : column, + columnIndex === index ? setIgnoreColumn(column) : column, ), ); }, @@ -109,7 +109,7 @@ export const MatchColumnsStep = ({ ); const onChange = useCallback( - (value: T, columnIndex: number) => { + (value: string, columnIndex: number) => { if (value === DO_NOT_IMPORT_OPTION_KEY) { if (columns[columnIndex].type === SpreadsheetColumnType.ignored) { onRevertIgnore(columnIndex); @@ -119,12 +119,12 @@ export const MatchColumnsStep = ({ } else { const field = fields.find( (field) => field.key === value, - ) as unknown as SpreadsheetImportField; + ) as unknown as SpreadsheetImportField; const existingFieldIndex = columns.findIndex( (column) => 'value' in column && column.value === field.key, ); setColumns( - columns.map>((column, index) => { + columns.map((column, index) => { if (columnIndex === index) { return setColumn(column, field, data); } else if (index === existingFieldIndex) { @@ -141,9 +141,9 @@ export const MatchColumnsStep = ({ const handleContinue = useCallback( async ( - values: ImportedStructuredRow[], + values: ImportedStructuredRow[], rawData: ImportedRow[], - columns: SpreadsheetColumns, + columns: SpreadsheetColumns, ) => { try { const data = await matchColumnsStepHook(values, rawData, columns); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/ColumnGrid.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/ColumnGrid.tsx index fdf4c3cd1..218f15baf 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/ColumnGrid.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/ColumnGrid.tsx @@ -82,28 +82,28 @@ const StyledGridHeader = styled.div` padding-right: ${({ theme }) => theme.spacing(4)}; `; -type ColumnGridProps = { - columns: SpreadsheetColumns; +type ColumnGridProps = { + columns: SpreadsheetColumns; renderUserColumn: ( - columns: SpreadsheetColumns, + columns: SpreadsheetColumns, columnIndex: number, ) => React.ReactNode; renderTemplateColumn: ( - columns: SpreadsheetColumns, + columns: SpreadsheetColumns, columnIndex: number, ) => React.ReactNode; renderUnmatchedColumn: ( - columns: SpreadsheetColumns, + columns: SpreadsheetColumns, columnIndex: number, ) => React.ReactNode; }; -export const ColumnGrid = ({ +export const ColumnGrid = ({ columns, renderUserColumn, renderTemplateColumn, renderUnmatchedColumn, -}: ColumnGridProps) => { +}: ColumnGridProps) => { return ( <> diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectDropdownButton.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectDropdownButton.tsx index 527ef49a1..79f90ef46 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectDropdownButton.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectDropdownButton.tsx @@ -16,20 +16,20 @@ const StyledIconChevronDown = styled(IconChevronDown)` color: ${({ theme }) => theme.font.color.tertiary}; `; -export type SubMatchingSelectDropdownButtonProps = { - option: SpreadsheetMatchedOptions | Partial>; +export type SubMatchingSelectDropdownButtonProps = { + option: SpreadsheetMatchedOptions | Partial; column: - | SpreadsheetMatchedSelectColumn - | SpreadsheetMatchedSelectOptionsColumn; + | SpreadsheetMatchedSelectColumn + | SpreadsheetMatchedSelectOptionsColumn; placeholder: string; }; -export const SubMatchingSelectDropdownButton = ({ +export const SubMatchingSelectDropdownButton = ({ option, column, placeholder, -}: SubMatchingSelectDropdownButtonProps) => { - const { fields } = useSpreadsheetImportInternal(); +}: SubMatchingSelectDropdownButtonProps) => { + const { spreadsheetImportFields: fields } = useSpreadsheetImportInternal(); const options = getFieldOptions(fields, column.value) as SelectOption[]; const value = options.find((opt) => opt.value === option.value); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectRow.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectRow.tsx index 3786c2733..7b823535e 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectRow.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectRow.tsx @@ -15,23 +15,23 @@ const StyledRowContainer = styled.div` padding-bottom: ${({ theme }) => theme.spacing(1)}; `; -interface SubMatchingSelectRowProps { - option: SpreadsheetMatchedOptions | Partial>; +interface SubMatchingSelectRowProps { + option: SpreadsheetMatchedOptions | Partial; column: - | SpreadsheetMatchedSelectColumn - | SpreadsheetMatchedSelectOptionsColumn; - onSubChange: (val: T, index: number, option: string) => void; + | SpreadsheetMatchedSelectColumn + | SpreadsheetMatchedSelectOptionsColumn; + onSubChange: (val: string, index: number, option: string) => void; placeholder: string; selectedOption?: - | SpreadsheetMatchedOptions - | Partial>; + | SpreadsheetMatchedOptions + | Partial; } -export const SubMatchingSelectRow = ({ +export const SubMatchingSelectRow = ({ option, column, onSubChange, placeholder, -}: SubMatchingSelectRowProps) => { +}: SubMatchingSelectRowProps) => { return ( diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectRowLeftSelect.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectRowLeftSelect.tsx index 0434cc777..487bf0b04 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectRowLeftSelect.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectRowLeftSelect.tsx @@ -15,13 +15,13 @@ const StyledControlLabel = styled.div` gap: ${({ theme }) => theme.spacing(1)}; `; -export type SubMatchingSelectRowLeftSelectProps = { - option: SpreadsheetMatchedOptions | Partial>; +export type SubMatchingSelectRowLeftSelectProps = { + option: SpreadsheetMatchedOptions | Partial; }; -export const SubMatchingSelectRowLeftSelect = ({ +export const SubMatchingSelectRowLeftSelect = ({ option, -}: SubMatchingSelectRowLeftSelectProps) => { +}: SubMatchingSelectRowLeftSelectProps) => { return ( diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectRowRightDropdown.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectRowRightDropdown.tsx index 69c52de5d..4098cef19 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectRowRightDropdown.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectRowRightDropdown.tsx @@ -18,34 +18,34 @@ const StyledDropdownContainer = styled.div` width: 100%; `; -interface SubMatchingSelectRowRightDropdownProps { - option: SpreadsheetMatchedOptions | Partial>; +interface SubMatchingSelectRowRightDropdownProps { + option: SpreadsheetMatchedOptions | Partial; column: - | SpreadsheetMatchedSelectColumn - | SpreadsheetMatchedSelectOptionsColumn; - onSubChange: (val: T, index: number, option: string) => void; + | SpreadsheetMatchedSelectColumn + | SpreadsheetMatchedSelectOptionsColumn; + onSubChange: (val: string, index: number, option: string) => void; placeholder: string; selectedOption?: - | SpreadsheetMatchedOptions - | Partial>; + | SpreadsheetMatchedOptions + | Partial; } -export const SubMatchingSelectRowRightDropdown = ({ +export const SubMatchingSelectRowRightDropdown = ({ option, column, onSubChange, placeholder, -}: SubMatchingSelectRowRightDropdownProps) => { +}: SubMatchingSelectRowRightDropdownProps) => { const dropdownId = `sub-matching-select-dropdown-${option.entry}`; const { closeDropdown } = useCloseDropdown(); - const { fields } = useSpreadsheetImportInternal(); + const { spreadsheetImportFields: fields } = useSpreadsheetImportInternal(); const options = getFieldOptions(fields, column.value) as SelectOption[]; const value = options.find((opt) => opt.value === option.value); const handleSelect = (selectedOption: SelectOption) => { - onSubChange(selectedOption.value as T, column.index, option.entry ?? ''); + onSubChange(selectedOption.value, column.index, option.entry ?? ''); closeDropdown(dropdownId); }; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/TemplateColumn.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/TemplateColumn.tsx index 4bb36d6af..4094bd570 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/TemplateColumn.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/TemplateColumn.tsx @@ -6,7 +6,7 @@ import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpre import { suggestedFieldsByColumnHeaderState } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/states/suggestedFieldsByColumnHeaderState'; import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType'; import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns'; -import { spreadsheetBuildFieldOptions } from '@/spreadsheet-import/utils/spreadsheetBuildFieldOptions'; +import { spreadsheetImportBuildFieldOptions } from '@/spreadsheet-import/utils/spreadsheetImportBuildFieldOptions'; import { useLingui } from '@lingui/react/macro'; import { useRecoilValue } from 'recoil'; import { IconForbid } from 'twenty-ui/display'; @@ -25,18 +25,18 @@ const StyledErrorMessage = styled.span` margin-top: ${({ theme }) => theme.spacing(1)}; `; -type TemplateColumnProps = { - columns: SpreadsheetColumns; +type TemplateColumnProps = { + columns: SpreadsheetColumns; columnIndex: number; - onChange: (val: T, index: number) => void; + onChange: (val: string, index: number) => void; }; -export const TemplateColumn = ({ +export const TemplateColumn = ({ columns, columnIndex, onChange, -}: TemplateColumnProps) => { - const { fields } = useSpreadsheetImportInternal(); +}: TemplateColumnProps) => { + const { spreadsheetImportFields: fields } = useSpreadsheetImportInternal(); const suggestedFieldsByColumnHeader = useRecoilValue( suggestedFieldsByColumnHeaderState, ); @@ -46,8 +46,8 @@ export const TemplateColumn = ({ const { t } = useLingui(); - const fieldOptions = spreadsheetBuildFieldOptions(fields, columns); - const suggestedFieldOptions = spreadsheetBuildFieldOptions( + const fieldOptions = spreadsheetImportBuildFieldOptions(fields, columns); + const suggestedFieldOptions = spreadsheetImportBuildFieldOptions( suggestedFieldsByColumnHeader[column.header] ?? [], columns, ); @@ -74,7 +74,7 @@ export const TemplateColumn = ({ onChange(value?.value as T, column.index)} + onChange={(value) => onChange(value?.value as string, column.index)} options={selectOptions} suggestedOptions={suggestedFieldOptions} columnIndex={column.index.toString()} diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumn.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumn.tsx index 55f4320d3..64699c6ea 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumn.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumn.tsx @@ -11,9 +11,9 @@ import { useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { AnimatedExpandableContainer } from 'twenty-ui/layout'; -const getExpandableContainerTitle = ( - fields: SpreadsheetImportFields, - column: SpreadsheetColumn, +const getExpandableContainerTitle = ( + fields: SpreadsheetImportFields, + column: SpreadsheetColumn, ) => { const fieldLabel = fields.find( (field) => 'value' in column && field.key === column.value, @@ -25,10 +25,10 @@ const getExpandableContainerTitle = ( } Unmatched)`; }; -type UnmatchColumnProps = { - columns: SpreadsheetColumns; +type UnmatchColumnProps = { + columns: SpreadsheetColumns; columnIndex: number; - onSubChange: (val: T, index: number, option: string) => void; + onSubChange: (val: string, index: number, option: string) => void; }; const StyledContainer = styled.div` @@ -44,12 +44,12 @@ const StyledContentWrapper = styled.div` padding-bottom: ${({ theme }) => theme.spacing(4)}; `; -export const UnmatchColumn = ({ +export const UnmatchColumn = ({ columns, columnIndex, onSubChange, -}: UnmatchColumnProps) => { - const { fields } = useSpreadsheetImportInternal(); +}: UnmatchColumnProps) => { + const { spreadsheetImportFields: fields } = useSpreadsheetImportInternal(); const [isExpanded, setIsExpanded] = useState(false); const column = columns[columnIndex]; const isSelect = 'matchedOptions' in column; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/UserTableColumn.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/UserTableColumn.tsx index 46215c495..410bb1d01 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/UserTableColumn.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/UserTableColumn.tsx @@ -29,15 +29,15 @@ const StyledExample = styled.span` white-space: nowrap; `; -type UserTableColumnProps = { - column: SpreadsheetColumn; +type UserTableColumnProps = { + column: SpreadsheetColumn; importedRow: ImportedRow; }; -export const UserTableColumn = ({ +export const UserTableColumn = ({ column, importedRow, -}: UserTableColumnProps) => { +}: UserTableColumnProps) => { const { header } = column; const firstDefinedValue = importedRow.find(isDefined); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/states/initialComputedColumnsState.ts b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/states/initialComputedColumnsState.ts index 0698e795b..51ade3b39 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/states/initialComputedColumnsState.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/states/initialComputedColumnsState.ts @@ -5,18 +5,18 @@ import { atom, selectorFamily } from 'recoil'; export const matchColumnsState = atom({ key: 'MatchColumnsState', - default: [] as SpreadsheetColumns, + default: [] as SpreadsheetColumns, }); export const initialComputedColumnsSelector = selectorFamily< - SpreadsheetColumns, + SpreadsheetColumns, ImportedRow >({ key: 'initialComputedColumnsSelector', get: (headerValues: ImportedRow) => ({ get }) => { - const currentState = get(matchColumnsState) as SpreadsheetColumns; + const currentState = get(matchColumnsState) as SpreadsheetColumns; if (currentState.length === 0) { // Do not remove spread, it indexes empty array elements, otherwise map() skips over them const initialState = ([...headerValues] as string[]).map( @@ -26,7 +26,7 @@ export const initialComputedColumnsSelector = selectorFamily< header: value ?? '', }), ); - return initialState as SpreadsheetColumns; + return initialState as SpreadsheetColumns; } else { return currentState; } @@ -34,6 +34,6 @@ export const initialComputedColumnsSelector = selectorFamily< set: () => ({ set }, newValue) => { - set(matchColumnsState, newValue as SpreadsheetColumns); + set(matchColumnsState, newValue as SpreadsheetColumns); }, }); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/states/suggestedFieldsByColumnHeaderState.ts b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/states/suggestedFieldsByColumnHeaderState.ts index 8ba18b3ed..4efcbba29 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/states/suggestedFieldsByColumnHeaderState.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/states/suggestedFieldsByColumnHeaderState.ts @@ -3,5 +3,5 @@ import { createState } from 'twenty-ui/utilities'; export const suggestedFieldsByColumnHeaderState = createState({ key: 'suggestedFieldsByColumnHeaderState', - defaultValue: {} as Record[]>, + defaultValue: {} as Record, }); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/columns.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/columns.tsx deleted file mode 100644 index 9ccedc314..000000000 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/columns.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import styled from '@emotion/styled'; -// @ts-expect-error // Todo: remove usage of react-data-grid -import { Column } from 'react-data-grid'; -import { createPortal } from 'react-dom'; - -import { SpreadsheetImportFields } from '@/spreadsheet-import/types'; -import { AppTooltip } from 'twenty-ui/display'; - -const StyledHeaderContainer = styled.div` - align-items: center; - display: flex; - gap: ${({ theme }) => theme.spacing(1)}; - position: relative; -`; - -const StyledHeaderLabel = styled.span` - display: flex; - flex: 1; - overflow: hidden; - text-overflow: ellipsis; -`; - -const StyledDefaultContainer = styled.div` - min-height: 100%; - min-width: 100%; - overflow: hidden; - text-overflow: ellipsis; -`; - -export const generateColumns = ( - fields: SpreadsheetImportFields, -) => - fields.map( - (column): Column => ({ - key: column.key, - name: column.label, - minWidth: 150, - headerRenderer: () => ( - - - {column.label} - - {column.description && - createPortal( - , - document.body, - )} - - ), - formatter: ({ row }: any) => ( - {row[column.key]} - ), - }), - ); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/hooks/useDownloadFakeRecords.ts b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/hooks/useDownloadFakeRecords.ts index 4c02ac94d..256cc54f9 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/hooks/useDownloadFakeRecords.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/hooks/useDownloadFakeRecords.ts @@ -1,5 +1,6 @@ import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; -import { spreadsheetImportFilterAvailableFieldMetadataItems } from '@/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts'; +import { spreadsheetImportFilterAvailableFieldMetadataItems } from '@/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems'; +import { getCompositeSubFieldLabelWithFieldLabel } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldLabelWithFieldLabel'; import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; import { SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs'; import { escapeCSVValue } from '@/spreadsheet-import/utils/escapeCSVValue'; @@ -58,8 +59,8 @@ export const useDownloadFakeRecords = () => { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[field.type].exampleValues; headerRow.push( - ...subFields.map( - ({ subFieldLabel }) => `${field.label} / ${subFieldLabel}`, + ...subFields.map(({ subFieldLabel }) => + getCompositeSubFieldLabelWithFieldLabel(field, subFieldLabel), ), ); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx index f2971c7ea..98a0cdead 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx @@ -1,5 +1,6 @@ import { SpreadsheetImportTable } from '@/spreadsheet-import/components/SpreadsheetImportTable'; import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton'; +import { useHideStepBar } from '@/spreadsheet-import/hooks/useHideStepBar'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep'; import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType'; @@ -91,30 +92,36 @@ const StyledNoRowsWithErrorsContainer = styled.div` margin: auto 0; `; -type ValidationStepProps = { - initialData: ImportedStructuredRow[]; - importedColumns: SpreadsheetColumns; +type ValidationStepProps = { + initialData: ImportedStructuredRow[]; + importedColumns: SpreadsheetColumns; file: File; onBack: () => void; setCurrentStepState: Dispatch>; }; -export const ValidationStep = ({ +export const ValidationStep = ({ initialData, importedColumns, file, setCurrentStepState, onBack, -}: ValidationStepProps) => { +}: ValidationStepProps) => { + const hideStepBar = useHideStepBar(); const { enqueueDialog } = useDialogManager(); - const { fields, onClose, onSubmit, rowHook, tableHook } = - useSpreadsheetImportInternal(); + const { + spreadsheetImportFields: fields, + onClose, + onSubmit, + rowHook, + tableHook, + } = useSpreadsheetImportInternal(); const [data, setData] = useState< - (ImportedStructuredRow & ImportedStructuredRowMetadata)[] + (ImportedStructuredRow & ImportedStructuredRowMetadata)[] >( useMemo( - () => addErrorsAndRunHooks(initialData, fields, rowHook, tableHook), + () => addErrorsAndRunHooks(initialData, fields, rowHook, tableHook), // eslint-disable-next-line react-hooks/exhaustive-deps [], ), @@ -126,7 +133,7 @@ export const ValidationStep = ({ const updateData = useCallback( (rows: typeof data) => { - setData(addErrorsAndRunHooks(rows, fields, rowHook, tableHook)); + setData(addErrorsAndRunHooks(rows, fields, rowHook, tableHook)); }, [setData, rowHook, tableHook, fields], ); @@ -205,8 +212,7 @@ export const ValidationStep = ({ }, [data, filterByErrors]); const rowKeyGetter = useCallback( - (row: ImportedStructuredRow & ImportedStructuredRowMetadata) => - row.__index, + (row: ImportedStructuredRow & ImportedStructuredRowMetadata) => row.__index, [], ); @@ -218,28 +224,29 @@ export const ValidationStep = ({ for (const key in __errors) { if (__errors[key].level === 'error') { acc.invalidStructuredRows.push( - values as unknown as ImportedStructuredRow, + values as unknown as ImportedStructuredRow, ); return acc; } } } acc.validStructuredRows.push( - values as unknown as ImportedStructuredRow, + values as unknown as ImportedStructuredRow, ); return acc; }, { - validStructuredRows: [] as ImportedStructuredRow[], - invalidStructuredRows: [] as ImportedStructuredRow[], + validStructuredRows: [] as ImportedStructuredRow[], + invalidStructuredRows: [] as ImportedStructuredRow[], allStructuredRows: data, - } satisfies SpreadsheetImportImportValidationResult, + } satisfies SpreadsheetImportImportValidationResult, ); setCurrentStepState({ type: SpreadsheetImportStepType.importData, recordsToImportCount: calculatedData.validStructuredRows.length, }); + hideStepBar(); await onSubmit(calculatedData, file); onClose(); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx index 8dc8379cd..f5aa86edb 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx @@ -71,9 +71,9 @@ const formatSafeId = (columnKey: string) => { return camelCase(columnKey.replace('(', '').replace(')', '')); }; -export const generateColumns = ( - fields: SpreadsheetImportFields, -): Column & ImportedStructuredRowMetadata>[] => [ +export const generateColumns = ( + fields: SpreadsheetImportFields, +): Column[] => [ { key: SELECT_COLUMN_KEY, name: '', @@ -108,7 +108,7 @@ export const generateColumns = ( ...fields.map( ( column, - ): Column & ImportedStructuredRowMetadata> => ({ + ): Column => ({ key: column.key, name: column.label, minWidth: 150, @@ -132,7 +132,7 @@ export const generateColumns = ( editable: column.fieldType.type !== 'checkbox', // Todo: remove usage of react-data-grid editor: ({ row, onRowChange, onClose }: any) => { - const columnKey = column.key as keyof (ImportedStructuredRow & + const columnKey = column.key as keyof (ImportedStructuredRow & ImportedStructuredRowMetadata); let component; @@ -166,7 +166,7 @@ export const generateColumns = ( }, // Todo: remove usage of react-data-grid formatter: ({ row, onRowChange }: { row: any; onRowChange: any }) => { - const columnKey = column.key as keyof (ImportedStructuredRow & + const columnKey = column.key as keyof (ImportedStructuredRow & ImportedStructuredRowMetadata); let component; @@ -197,7 +197,7 @@ export const generateColumns = ( id={formatSafeId(`${columnKey}-${row.__index}`)} > {column.fieldType.options.find( - (option) => option.value === row[columnKey as T], + (option) => option.value === row[columnKey], )?.label || null} ); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/types/SpreadsheetImportStep.ts b/packages/twenty-front/src/modules/spreadsheet-import/steps/types/SpreadsheetImportStep.ts index 22f2292c4..6a405c864 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/types/SpreadsheetImportStep.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/types/SpreadsheetImportStep.ts @@ -23,7 +23,7 @@ export type SpreadsheetImportStep = | { type: SpreadsheetImportStepType.validateData; data: any[]; - importedColumns: SpreadsheetColumns; + importedColumns: SpreadsheetColumns; } | { type: SpreadsheetImportStepType.loading; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumn.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumn.ts index 74404b854..74b2204b4 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumn.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumn.ts @@ -13,49 +13,49 @@ type SpreadsheetIgnoredColumn = { header: string; }; -type SpreadsheetMatchedColumn = { +type SpreadsheetMatchedColumn = { type: SpreadsheetColumnType.matched; index: number; header: string; - value: T; + value: string; }; -type SpreadsheetMatchedSwitchColumn = { +type SpreadsheetMatchedSwitchColumn = { type: SpreadsheetColumnType.matchedCheckbox; index: number; header: string; - value: T; + value: string; }; -export type SpreadsheetMatchedSelectColumn = { +export type SpreadsheetMatchedSelectColumn = { type: SpreadsheetColumnType.matchedSelect; index: number; header: string; - value: T; - matchedOptions: Partial>[]; + value: string; + matchedOptions: Partial[]; }; -export type SpreadsheetMatchedSelectOptionsColumn = { +export type SpreadsheetMatchedSelectOptionsColumn = { type: SpreadsheetColumnType.matchedSelectOptions; index: number; header: string; - value: T; - matchedOptions: SpreadsheetMatchedOptions[]; + value: string; + matchedOptions: SpreadsheetMatchedOptions[]; }; -export type SpreadsheetErrorColumn = { +export type SpreadsheetErrorColumn = { type: SpreadsheetColumnType.matchedError; index: number; header: string; - value: T; + value: string; errorMessage: string; }; -export type SpreadsheetColumn = +export type SpreadsheetColumn = | SpreadsheetEmptyColumn | SpreadsheetIgnoredColumn - | SpreadsheetMatchedColumn - | SpreadsheetMatchedSwitchColumn - | SpreadsheetMatchedSelectColumn - | SpreadsheetMatchedSelectOptionsColumn - | SpreadsheetErrorColumn; + | SpreadsheetMatchedColumn + | SpreadsheetMatchedSwitchColumn + | SpreadsheetMatchedSelectColumn + | SpreadsheetMatchedSelectOptionsColumn + | SpreadsheetErrorColumn; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumns.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumns.ts index e53cfb5e0..0e85854ca 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumns.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumns.ts @@ -1,3 +1,3 @@ import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn'; -export type SpreadsheetColumns = SpreadsheetColumn[]; +export type SpreadsheetColumns = SpreadsheetColumn[]; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportDialogOptions.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportDialogOptions.ts index e308df5c6..457bef90c 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportDialogOptions.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportDialogOptions.ts @@ -8,11 +8,11 @@ import { SpreadsheetImportRowHook } from '@/spreadsheet-import/types/Spreadsheet import { SpreadsheetImportTableHook } from '@/spreadsheet-import/types/SpreadsheetImportTableHook'; import { SpreadsheetImportStep } from '../steps/types/SpreadsheetImportStep'; -export type SpreadsheetImportDialogOptions = { +export type SpreadsheetImportDialogOptions = { // callback when RSI is closed before final submit onClose: () => void; // Field description for requested data - fields: SpreadsheetImportFields; + spreadsheetImportFields: SpreadsheetImportFields; // Runs after file upload step, receives and returns raw sheet data uploadStepHook?: (importedRows: ImportedRow[]) => Promise; // Runs after header selection step, receives and returns raw sheet data @@ -22,17 +22,17 @@ export type SpreadsheetImportDialogOptions = { ) => Promise<{ headerRow: ImportedRow; importedRows: ImportedRow[] }>; // Runs once before validation step, used for data mutations and if you want to change how columns were matched matchColumnsStepHook?: ( - importedStructuredRows: ImportedStructuredRow[], + importedStructuredRows: ImportedStructuredRow[], importedRows: ImportedRow[], - columns: SpreadsheetColumns, - ) => Promise[]>; + columns: SpreadsheetColumns, + ) => Promise; // Runs after column matching and on entry change - rowHook?: SpreadsheetImportRowHook; + rowHook?: SpreadsheetImportRowHook; // Runs after column matching and on entry change - tableHook?: SpreadsheetImportTableHook; + tableHook?: SpreadsheetImportTableHook; // Function called after user finishes the flow onSubmit: ( - validationResult: SpreadsheetImportImportValidationResult, + validationResult: SpreadsheetImportImportValidationResult, file: File, ) => Promise; // Function called when user aborts the importing flow @@ -59,5 +59,6 @@ export type SpreadsheetImportDialogOptions = { rtl?: boolean; // Allow header selection selectHeader?: boolean; + // Available field for import availableFieldMetadataItems: FieldMetadataItem[]; }; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportField.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportField.ts index 02e8d43de..320df13e9 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportField.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportField.ts @@ -1,25 +1,34 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { SpreadsheetImportFieldType } from '@/spreadsheet-import/types/SpreadsheetImportFieldType'; import { SpreadsheetImportFieldValidationDefinition } from '@/spreadsheet-import/types/SpreadsheetImportFieldValidationDefinition'; import { FieldMetadataType } from 'twenty-shared/types'; import { IconComponent } from 'twenty-ui/display'; -export type SpreadsheetImportField = { +export type SpreadsheetImportField = { // Icon Icon: IconComponent | null | undefined; // UI-facing field label label: string; // Field's unique identifier - key: T; + key: string; + // Field's metadata item id - same for all associated nested fields + fieldMetadataItemId: string; // UI-facing additional information displayed via tooltip and ? icon description?: string; - // Alternate labels used for fields' auto-matching, e.g. "fname" -> "firstName" - alternateMatches?: string[]; // Validations used for field entries fieldValidationDefinitions?: SpreadsheetImportFieldValidationDefinition[]; // Field entry component, default: Input fieldType: SpreadsheetImportFieldType; // Field metadata type fieldMetadataType: FieldMetadataType; - // UI-facing values shown to user as field examples pre-upload phase - example?: string; + // if true, it can be a composite sub-field or a relation connect field (or both) + isNestedField: boolean; + // can be true only if isNestedField is true + isCompositeSubField?: boolean; + // defined only if isCompositeSubField is true + compositeSubFieldKey?: string; + // can be true only if isNestedField is true + isRelationConnectField?: boolean; + // defined only if isRelationConnectField is true + uniqueFieldMetadataItem?: FieldMetadataItem; }; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFieldOption.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFieldOption.ts new file mode 100644 index 000000000..493398481 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFieldOption.ts @@ -0,0 +1,12 @@ +import { IconComponent } from 'twenty-ui/display'; + +export type SpreadsheetImportFieldOption = { + Icon: IconComponent | null | undefined; + value: string; + label: string; + shortLabelForNestedField?: string; + disabled?: boolean; + fieldMetadataTypeLabel?: string; + isNestedField?: boolean; + fieldMetadataItemId?: string; +}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFields.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFields.ts index 8a1549867..369b98e0b 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFields.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFields.ts @@ -1,6 +1,4 @@ import { SpreadsheetImportField } from '@/spreadsheet-import/types/SpreadsheetImportField'; import { ReadonlyDeep } from 'type-fest'; -export type SpreadsheetImportFields = ReadonlyDeep< - SpreadsheetImportField[] ->; +export type SpreadsheetImportFields = ReadonlyDeep; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportValidationResult.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportValidationResult.ts index d2016772a..378d58469 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportValidationResult.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportValidationResult.ts @@ -1,9 +1,8 @@ import { ImportedStructuredRowMetadata } from '@/spreadsheet-import/steps/components/ValidationStep/types'; import { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow'; -export type SpreadsheetImportImportValidationResult = { - validStructuredRows: ImportedStructuredRow[]; - invalidStructuredRows: ImportedStructuredRow[]; - allStructuredRows: (ImportedStructuredRow & - ImportedStructuredRowMetadata)[]; +export type SpreadsheetImportImportValidationResult = { + validStructuredRows: ImportedStructuredRow[]; + invalidStructuredRows: ImportedStructuredRow[]; + allStructuredRows: (ImportedStructuredRow & ImportedStructuredRowMetadata)[]; }; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportedStructuredRow.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportedStructuredRow.ts index 1c428117b..a62b02897 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportedStructuredRow.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportedStructuredRow.ts @@ -1,3 +1,3 @@ -export type ImportedStructuredRow = { - [key in T]: string | boolean | undefined; +export type ImportedStructuredRow = { + [key: string]: string | boolean | undefined; }; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportRowHook.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportRowHook.ts index a3db443fe..1d81fa5e0 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportRowHook.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportRowHook.ts @@ -1,8 +1,8 @@ import { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow'; import { SpreadsheetImportInfo } from './SpreadsheetImportInfo'; -export type SpreadsheetImportRowHook = ( - row: ImportedStructuredRow, - addError: (fieldKey: T, error: SpreadsheetImportInfo) => void, - table: ImportedStructuredRow[], -) => ImportedStructuredRow; +export type SpreadsheetImportRowHook = ( + row: ImportedStructuredRow, + addError: (fieldKey: string, error: SpreadsheetImportInfo) => void, + table: ImportedStructuredRow[], +) => ImportedStructuredRow; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportTableHook.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportTableHook.ts index 7459de943..17b44f134 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportTableHook.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportTableHook.ts @@ -1,11 +1,11 @@ import { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow'; import { SpreadsheetImportInfo } from './SpreadsheetImportInfo'; -export type SpreadsheetImportTableHook = ( - table: ImportedStructuredRow[], +export type SpreadsheetImportTableHook = ( + table: ImportedStructuredRow[], addError: ( rowIndex: number, - fieldKey: T, + fieldKey: string, error: SpreadsheetImportInfo, ) => void, -) => ImportedStructuredRow[]; +) => ImportedStructuredRow[]; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetMatchedOptions.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetMatchedOptions.ts index 98beecf99..a7d124363 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetMatchedOptions.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetMatchedOptions.ts @@ -1,4 +1,4 @@ -export type SpreadsheetMatchedOptions = { +export type SpreadsheetMatchedOptions = { entry: string; - value?: T; + value?: string; }; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/dataMutations.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/dataMutations.test.ts index 5d62ad5bf..776622de6 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/dataMutations.test.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/dataMutations.test.ts @@ -9,17 +9,16 @@ import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations'; import { FieldMetadataType } from 'twenty-shared/types'; describe('addErrorsAndRunHooks', () => { - type FullData = ImportedStructuredRow<'name' | 'age' | 'country'>; - const requiredField: SpreadsheetImportField<'name'> = { + const requiredField = { key: 'name', label: 'Name', fieldValidationDefinitions: [{ rule: 'required' }], Icon: null, fieldType: { type: 'input' }, fieldMetadataType: FieldMetadataType.TEXT, - }; + } as SpreadsheetImportField; - const regexField: SpreadsheetImportField<'age'> = { + const regexField = { key: 'age', label: 'Age', fieldValidationDefinitions: [ @@ -28,18 +27,20 @@ describe('addErrorsAndRunHooks', () => { Icon: null, fieldType: { type: 'input' }, fieldMetadataType: FieldMetadataType.NUMBER, - }; + } as SpreadsheetImportField; - const uniqueField: SpreadsheetImportField<'country'> = { + const uniqueField = { key: 'country', label: 'Country', fieldValidationDefinitions: [{ rule: 'unique' }], Icon: null, fieldType: { type: 'input' }, fieldMetadataType: FieldMetadataType.SELECT, - }; + fieldMetadataItemId: '2', + isNestedField: false, + } as SpreadsheetImportField; - const functionValidationFieldTrue: SpreadsheetImportField<'email'> = { + const functionValidationFieldTrue = { key: 'email', label: 'Email', fieldValidationDefinitions: [ @@ -52,9 +53,11 @@ describe('addErrorsAndRunHooks', () => { Icon: null, fieldType: { type: 'input' }, fieldMetadataType: FieldMetadataType.EMAILS, - }; + fieldMetadataItemId: '1', + isNestedField: false, + } as SpreadsheetImportField; - const functionValidationFieldFalse: SpreadsheetImportField<'email'> = { + const functionValidationFieldFalse = { key: 'email', label: 'Email', fieldValidationDefinitions: [ @@ -67,23 +70,25 @@ describe('addErrorsAndRunHooks', () => { Icon: null, fieldType: { type: 'input' }, fieldMetadataType: FieldMetadataType.EMAILS, - }; + fieldMetadataItemId: '3', + isNestedField: false, + } as SpreadsheetImportField; - const validData: ImportedStructuredRow<'name' | 'age'> = { + const validData: ImportedStructuredRow = { name: 'John', age: '30', }; - const dataWithoutNameAndInvalidAge: ImportedStructuredRow<'name' | 'age'> = { + const dataWithoutNameAndInvalidAge: ImportedStructuredRow = { name: '', age: 'Invalid', }; - const dataWithDuplicatedValue: FullData = { + const dataWithDuplicatedValue: ImportedStructuredRow = { name: 'Alice', age: '40', country: 'Brazil', }; - const data: ImportedStructuredRow<'name' | 'age'>[] = [ + const data: ImportedStructuredRow[] = [ validData, dataWithoutNameAndInvalidAge, ]; @@ -113,18 +118,14 @@ describe('addErrorsAndRunHooks', () => { level: 'error', }; - const rowHook: SpreadsheetImportRowHook<'name' | 'age'> = jest.fn( - (row, addError) => { - addError('name', nameError); - return row; - }, - ); - const tableHook: SpreadsheetImportTableHook<'name' | 'age'> = jest.fn( - (table, addError) => { - addError(0, 'age', ageError); - return table; - }, - ); + const rowHook: SpreadsheetImportRowHook = jest.fn((row, addError) => { + addError('name', nameError); + return row; + }); + const tableHook: SpreadsheetImportTableHook = jest.fn((table, addError) => { + addError(0, 'age', ageError); + return table; + }); it('should correctly call rowHook and tableHook and add errors', () => { const result = addErrorsAndRunHooks( @@ -179,7 +180,7 @@ describe('addErrorsAndRunHooks', () => { [ dataWithDuplicatedValue, dataWithDuplicatedValue, - ] as unknown as FullData[], + ] as unknown as ImportedStructuredRow[], [uniqueField], ); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findMatch.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findMatch.test.ts deleted file mode 100644 index 84c1c0cc4..000000000 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findMatch.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { SpreadsheetImportField } from '@/spreadsheet-import/types'; -import { findMatch } from '@/spreadsheet-import/utils/findMatch'; -import { FieldMetadataType } from 'twenty-shared/types'; - -describe('findMatch', () => { - const defaultField: SpreadsheetImportField<'defaultField'> = { - key: 'defaultField', - Icon: null, - label: 'label', - fieldType: { - type: 'input', - }, - fieldMetadataType: FieldMetadataType.TEXT, - alternateMatches: ['Full Name', 'First Name'], - }; - - const secondaryField: SpreadsheetImportField<'secondaryField'> = { - key: 'secondaryField', - Icon: null, - label: 'label', - fieldType: { - type: 'input', - }, - fieldMetadataType: FieldMetadataType.TEXT, - }; - - const fields = [defaultField, secondaryField]; - - it('should return the matching field if the header matches exactly with the key', () => { - const autoMapDistance = 0; - - const result = findMatch(defaultField.key, fields, autoMapDistance); - - expect(result).toBe(defaultField.key); - }); - - it('should return the matching field if the header matches exactly one of the alternate matches', () => { - const autoMapDistance = 0; - - const result = findMatch( - defaultField.alternateMatches?.[0] ?? '', - fields, - autoMapDistance, - ); - - expect(result).toBe(defaultField.key); - }); - - it('should return the matching field if the header matches partially one of the alternate matches', () => { - const header = 'First'; - const autoMapDistance = 5; - - const result = findMatch(header, fields, autoMapDistance); - - expect(result).toBe(defaultField.key); - }); - - it('should return the matching field if the header matches partially both of the alternate matches', () => { - const header = 'Name'; - const autoMapDistance = 5; - - const result = findMatch(header, fields, autoMapDistance); - - expect(result).toBe(defaultField.key); - }); - - it('should return undefined if no exact match or alternate match is found within the auto map distance', () => { - const header = 'Header'; - const autoMapDistance = 2; - - const result = findMatch(header, fields, autoMapDistance); - - expect(result).toBeUndefined(); - }); - - it('should return the matching field with the smallest Levenshtein distance if within auto map distance', () => { - const header = 'Name'.split('').reverse().join(''); - const autoMapDistance = 100; - - const result = findMatch(header, fields, autoMapDistance); - - expect(result).toBe(defaultField.key); - }); - - it('should return undefined if no match is found within auto map distance', () => { - const header = 'Name'.split('').reverse().join(''); - const autoMapDistance = 1; - - const result = findMatch(header, fields, autoMapDistance); - - expect(result).toBeUndefined(); - }); -}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findUnmatchedRequiredFields.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findUnmatchedRequiredFields.test.ts index f6c875249..ed1f8e8c9 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findUnmatchedRequiredFields.test.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findUnmatchedRequiredFields.test.ts @@ -7,7 +7,7 @@ import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetCol import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields'; import { FieldMetadataType } from 'twenty-shared/types'; -const nameField: SpreadsheetImportField<'Name'> = { +const nameField: SpreadsheetImportField = { key: 'Name', label: 'Name', Icon: null, @@ -15,9 +15,11 @@ const nameField: SpreadsheetImportField<'Name'> = { type: 'input', }, fieldMetadataType: FieldMetadataType.TEXT, + fieldMetadataItemId: '1', + isNestedField: false, }; -const ageField: SpreadsheetImportField<'Age'> = { +const ageField: SpreadsheetImportField = { key: 'Age', label: 'Age', Icon: null, @@ -25,37 +27,37 @@ const ageField: SpreadsheetImportField<'Age'> = { type: 'input', }, fieldMetadataType: FieldMetadataType.NUMBER, + fieldMetadataItemId: '2', + isNestedField: false, }; const validations: SpreadsheetImportFieldValidationDefinition[] = [ { rule: 'required' }, ]; -const nameFieldWithValidations: SpreadsheetImportField<'Name'> = { +const nameFieldWithValidations: SpreadsheetImportField = { ...nameField, fieldValidationDefinitions: validations, }; -const ageFieldWithValidations: SpreadsheetImportField<'Age'> = { +const ageFieldWithValidations: SpreadsheetImportField = { ...ageField, fieldValidationDefinitions: validations, }; -type ColumnValues = 'Name' | 'Age'; - -const nameColumn: SpreadsheetColumn = { +const nameColumn: SpreadsheetColumn = { type: SpreadsheetColumnType.matched, index: 0, header: '', value: 'Name', }; -const ageColumn: SpreadsheetColumn = { +const ageColumn: SpreadsheetColumn = { type: SpreadsheetColumnType.matched, index: 0, header: '', value: 'Age', }; -const extraColumn: SpreadsheetColumn = { +const extraColumn: SpreadsheetColumn = { type: SpreadsheetColumnType.matched, index: 0, header: '', diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getFieldOptions.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getFieldOptions.test.ts index 8b770c964..cfdc07610 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getFieldOptions.test.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getFieldOptions.test.ts @@ -17,7 +17,7 @@ describe('getFieldOptions', () => { value: 'Three', }, ]; - const fields: SpreadsheetImportField<'Options' | 'Name'>[] = [ + const fields: SpreadsheetImportField[] = [ { key: 'Options', Icon: null, @@ -27,6 +27,8 @@ describe('getFieldOptions', () => { options: optionsArray, }, fieldMetadataType: FieldMetadataType.SELECT, + fieldMetadataItemId: '1', + isNestedField: false, }, { key: 'Name', @@ -36,6 +38,8 @@ describe('getFieldOptions', () => { type: 'input', }, fieldMetadataType: FieldMetadataType.TEXT, + fieldMetadataItemId: '2', + isNestedField: false, }, ]; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getMatchedColumns.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getMatchedColumns.test.ts deleted file mode 100644 index 82b8e063d..000000000 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getMatchedColumns.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { SpreadsheetImportField } from '@/spreadsheet-import/types'; -import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn'; -import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType'; -import { getMatchedColumns } from '@/spreadsheet-import/utils/getMatchedColumns'; -import { FieldMetadataType } from 'twenty-shared/types'; - -describe('getMatchedColumns', () => { - const columns: SpreadsheetColumn[] = [ - { - index: 0, - header: 'Name', - type: SpreadsheetColumnType.matched, - value: 'Name', - }, - { - index: 1, - header: 'Location', - type: SpreadsheetColumnType.matched, - value: 'Location', - }, - { - index: 2, - header: 'Age', - type: SpreadsheetColumnType.matched, - value: 'Age', - }, - ]; - - const fields: SpreadsheetImportField[] = [ - { - key: 'Name', - label: 'Name', - fieldType: { type: 'input' }, - fieldMetadataType: FieldMetadataType.TEXT, - Icon: null, - }, - { - key: 'Location', - label: 'Location', - fieldType: { type: 'select', options: [] }, - fieldMetadataType: FieldMetadataType.POSITION, - Icon: null, - }, - { - key: 'Age', - label: 'Age', - fieldType: { type: 'input' }, - fieldMetadataType: FieldMetadataType.NUMBER, - Icon: null, - }, - ]; - - const data = [ - ['John', 'New York'], - ['Alice', 'Los Angeles'], - ]; - - const autoMapDistance = 2; - - it('should return matched columns for each field', () => { - const result = getMatchedColumns(columns, fields, data, autoMapDistance); - expect(result).toEqual([ - { - index: 0, - header: 'Name', - type: SpreadsheetColumnType.matched, - value: 'Name', - }, - { - index: 1, - header: 'Location', - type: SpreadsheetColumnType.matchedSelect, - value: 'Location', - matchedOptions: [ - { - entry: 'New York', - }, - { - entry: 'Los Angeles', - }, - ], - }, - { - index: 2, - header: 'Age', - type: SpreadsheetColumnType.matched, - value: 'Age', - }, - ]); - }); - - it('should handle columns with duplicate values by choosing the closest match', () => { - const columnsWithDuplicates: SpreadsheetColumn[] = [ - { - index: 0, - header: 'Name', - type: SpreadsheetColumnType.matched, - value: 'Name', - }, - { - index: 1, - header: 'Name', - type: SpreadsheetColumnType.matched, - value: 'Name', - }, - { - index: 2, - header: 'Location', - type: SpreadsheetColumnType.matched, - value: 'Location', - }, - ]; - - const result = getMatchedColumns( - columnsWithDuplicates, - fields, - data, - autoMapDistance, - ); - - expect(result[0]).toEqual({ - index: 0, - header: 'Name', - type: SpreadsheetColumnType.empty, - }); - expect(result[1]).toEqual({ - index: 1, - header: 'Name', - type: SpreadsheetColumnType.matched, - value: 'Name', - }); - }); - - it('should return initial columns when no auto match is found', () => { - const unmatchedColumnsData: string[][] = [ - ['John', 'New York', '30'], - ['Alice', 'Los Angeles', '25'], - ]; - - const unmatchedFields: SpreadsheetImportField[] = [ - { - key: 'Hobby', - label: 'Hobby', - fieldType: { type: 'input' }, - fieldMetadataType: FieldMetadataType.TEXT, - Icon: null, - }, - { - key: 'Interest', - label: 'Interest', - fieldType: { type: 'input' }, - fieldMetadataType: FieldMetadataType.TEXT, - Icon: null, - }, - ]; - - const result = getMatchedColumns( - columns, - unmatchedFields, - unmatchedColumnsData, - autoMapDistance, - ); - - expect(result).toEqual(columns); - }); -}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/normalizeTableData.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/normalizeTableData.test.ts index 527f7ca1c..5d242a3b2 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/normalizeTableData.test.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/normalizeTableData.test.ts @@ -6,7 +6,7 @@ import { normalizeTableData } from '@/spreadsheet-import/utils/normalizeTableDat import { FieldMetadataType } from 'twenty-shared/types'; describe('normalizeTableData', () => { - const columns: SpreadsheetColumn[] = [ + const columns: SpreadsheetColumn[] = [ { index: 0, header: 'Name', @@ -27,7 +27,7 @@ describe('normalizeTableData', () => { }, ]; - const fields: SpreadsheetImportField[] = [ + const fields = [ { key: 'name', label: 'Name', @@ -51,7 +51,7 @@ describe('normalizeTableData', () => { fieldMetadataType: FieldMetadataType.BOOLEAN, Icon: null, }, - ]; + ] as SpreadsheetImportField[]; const rawData = [ ['John', '30', 'Yes'], @@ -70,7 +70,7 @@ describe('normalizeTableData', () => { }); it('should normalize matchedCheckbox values and handle booleanMatches', () => { - const columns: SpreadsheetColumn[] = [ + const columns: SpreadsheetColumn[] = [ { index: 0, header: 'Active', @@ -79,7 +79,7 @@ describe('normalizeTableData', () => { }, ]; - const fields: SpreadsheetImportField[] = [ + const fields = [ { key: 'active', label: 'Active', @@ -89,8 +89,10 @@ describe('normalizeTableData', () => { }, fieldMetadataType: FieldMetadataType.BOOLEAN, Icon: null, + fieldMetadataItemId: '1', + isNestedField: false, }, - ]; + ] as SpreadsheetImportField[]; const rawData = [['Yes'], ['No'], ['OtherValue']]; @@ -100,7 +102,7 @@ describe('normalizeTableData', () => { }); it('should map matchedSelect and matchedSelectOptions values correctly', () => { - const columns: SpreadsheetColumn[] = [ + const columns: SpreadsheetColumn[] = [ { index: 0, header: 'Number', @@ -113,7 +115,7 @@ describe('normalizeTableData', () => { }, ]; - const fields: SpreadsheetImportField[] = [ + const fields = [ { key: 'number', label: 'Number', @@ -127,7 +129,7 @@ describe('normalizeTableData', () => { fieldMetadataType: FieldMetadataType.SELECT, Icon: null, }, - ]; + ] as SpreadsheetImportField[]; const rawData = [['One'], ['Two'], ['OtherValue']]; @@ -141,7 +143,7 @@ describe('normalizeTableData', () => { }); it('should handle empty and ignored columns', () => { - const columns: SpreadsheetColumn[] = [ + const columns: SpreadsheetColumn[] = [ { index: 0, header: 'Empty', type: SpreadsheetColumnType.empty }, { index: 1, header: 'Ignored', type: SpreadsheetColumnType.ignored }, ]; @@ -154,7 +156,7 @@ describe('normalizeTableData', () => { }); it('should handle unrecognized column types and return empty object', () => { - const columns: SpreadsheetColumns = [ + const columns: SpreadsheetColumns = [ { index: 0, header: 'Unrecognized', diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setColumn.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setColumn.test.ts index 0aae11bab..fa6a87c4e 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setColumn.test.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setColumn.test.ts @@ -5,15 +5,15 @@ import { setColumn } from '@/spreadsheet-import/utils/setColumn'; import { FieldMetadataType } from 'twenty-shared/types'; describe('setColumn', () => { - const defaultField: SpreadsheetImportField<'Name'> = { + const defaultField = { Icon: null, label: 'label', key: 'Name', fieldType: { type: 'input' }, fieldMetadataType: FieldMetadataType.TEXT, - }; + } as SpreadsheetImportField; - const oldColumn: SpreadsheetColumn<'oldValue'> = { + const oldColumn: SpreadsheetColumn = { index: 0, header: 'Name', type: SpreadsheetColumnType.matched, @@ -27,7 +27,7 @@ describe('setColumn', () => { type: 'select', options: [{ value: 'John' }, { value: 'Alice' }], }, - } as SpreadsheetImportField<'Name'>; + } as SpreadsheetImportField; const data = [['John'], ['Alice']]; const result = setColumn(oldColumn, field, data); @@ -54,7 +54,7 @@ describe('setColumn', () => { const field = { ...defaultField, fieldType: { type: 'checkbox' }, - } as SpreadsheetImportField<'Name'>; + } as SpreadsheetImportField; const result = setColumn(oldColumn, field); @@ -70,7 +70,7 @@ describe('setColumn', () => { const field = { ...defaultField, fieldType: { type: 'input' }, - } as SpreadsheetImportField<'Name'>; + } as SpreadsheetImportField; const result = setColumn(oldColumn, field); @@ -86,7 +86,7 @@ describe('setColumn', () => { const field = { ...defaultField, fieldType: { type: 'unknown' }, - } as unknown as SpreadsheetImportField<'Name'>; + } as unknown as SpreadsheetImportField; const result = setColumn(oldColumn, field); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setIgnoreColumn.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setIgnoreColumn.test.ts index edf3345f9..dddf0868c 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setIgnoreColumn.test.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setIgnoreColumn.test.ts @@ -4,7 +4,7 @@ import { setIgnoreColumn } from '@/spreadsheet-import/utils/setIgnoreColumn'; describe('setIgnoreColumn', () => { it('should return a column with type "ignored"', () => { - const column: SpreadsheetColumn<'John'> = { + const column: SpreadsheetColumn = { index: 0, header: 'Name', type: SpreadsheetColumnType.matched, diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setSubColumn.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setSubColumn.test.ts index e764f4b6e..e08bf6713 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setSubColumn.test.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setSubColumn.test.ts @@ -4,7 +4,7 @@ import { setSubColumn } from '@/spreadsheet-import/utils/setSubColumn'; describe('setSubColumn', () => { it('should return a matchedSelectColumn with updated matchedOptions', () => { - const oldColumn: SpreadsheetColumn<'John' | ''> = { + const oldColumn: SpreadsheetColumn = { index: 0, header: 'Name', type: SpreadsheetColumnType.matchedSelect, @@ -32,7 +32,7 @@ describe('setSubColumn', () => { }); it('should return a matchedSelectOptionsColumn with updated matchedOptions', () => { - const oldColumn: SpreadsheetColumn<'John' | 'Jane'> = { + const oldColumn: SpreadsheetColumn = { index: 0, header: 'Name', type: SpreadsheetColumnType.matchedSelectOptions, diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/dataMutations.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/dataMutations.ts index c06cebfde..ef509e39d 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/dataMutations.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/dataMutations.ts @@ -15,17 +15,17 @@ import { import { isDefined } from 'twenty-shared/utils'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; -export const addErrorsAndRunHooks = ( - data: (ImportedStructuredRow & Partial)[], - fields: SpreadsheetImportFields, - rowHook?: SpreadsheetImportRowHook, - tableHook?: SpreadsheetImportTableHook, -): (ImportedStructuredRow & ImportedStructuredRowMetadata)[] => { +export const addErrorsAndRunHooks = ( + data: (ImportedStructuredRow & Partial)[], + fields: SpreadsheetImportFields, + rowHook?: SpreadsheetImportRowHook, + tableHook?: SpreadsheetImportTableHook, +): (ImportedStructuredRow & ImportedStructuredRowMetadata)[] => { const errors: Errors = {}; const addHookError = ( rowIndex: number, - fieldKey: T, + fieldKey: string, error: SpreadsheetImportInfo, ) => { errors[rowIndex] = { @@ -48,7 +48,7 @@ export const addErrorsAndRunHooks = ( field.fieldValidationDefinitions?.forEach((fieldValidationDefinition) => { switch (fieldValidationDefinition.rule) { case 'unique': { - const values = data.map((entry) => entry[field.key as T]); + const values = data.map((entry) => entry[field.key]); const taken = new Set(); // Set of items used at least once const duplicates = new Set(); // Set of items used multiple times @@ -87,9 +87,9 @@ export const addErrorsAndRunHooks = ( case 'required': { data.forEach((entry, index) => { if ( - entry[field.key as T] === null || - entry[field.key as T] === undefined || - entry[field.key as T] === '' + entry[field.key] === null || + entry[field.key] === undefined || + entry[field.key] === '' ) { errors[index] = { ...errors[index], @@ -156,14 +156,17 @@ export const addErrorsAndRunHooks = ( if (!('__index' in value)) { value.__index = v4(); } - const newValue = value as ImportedStructuredRow & + const newValue = value as ImportedStructuredRow & ImportedStructuredRowMetadata; if (isDefined(errors[index])) { - return { ...newValue, __errors: errors[index] }; + return { ...newValue, __errors: errors[index] } as ImportedStructuredRow & + ImportedStructuredRowMetadata; } + if (isUndefinedOrNull(errors[index]) && isDefined(value?.__errors)) { - return { ...newValue, __errors: null }; + return { ...newValue, __errors: null } as ImportedStructuredRow & + ImportedStructuredRowMetadata; } return newValue; }); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/findMatch.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/findMatch.ts deleted file mode 100644 index 6a6659697..000000000 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/findMatch.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { SpreadsheetImportFields } from '@/spreadsheet-import/types'; -import lavenstein from 'js-levenshtein'; - -type AutoMatchAccumulator = { - distance: number; - value: T; -}; - -export const findMatch = ( - header: string, - fields: SpreadsheetImportFields, - autoMapDistance: number, -): T | undefined => { - const smallestValue = fields.reduce>((acc, field) => { - const distance = Math.min( - ...[ - lavenstein(field.key, header), - ...(field.alternateMatches?.map((alternate) => - lavenstein(alternate, header), - ) || []), - ], - ); - return distance < acc.distance || acc.distance === undefined - ? ({ value: field.key, distance } as AutoMatchAccumulator) - : acc; - }, {} as AutoMatchAccumulator); - return smallestValue.distance <= autoMapDistance - ? smallestValue.value - : undefined; -}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts index f31908931..b029fb7ed 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts @@ -1,9 +1,9 @@ import { SpreadsheetImportFields } from '@/spreadsheet-import/types'; import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns'; -export const findUnmatchedRequiredFields = ( - fields: SpreadsheetImportFields, - columns: SpreadsheetColumns, +export const findUnmatchedRequiredFields = ( + fields: SpreadsheetImportFields, + columns: SpreadsheetColumns, ) => fields .filter((field) => diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/getFieldOptions.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/getFieldOptions.ts index eb867c25f..6dc0e214a 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/getFieldOptions.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/getFieldOptions.ts @@ -1,7 +1,7 @@ import { SpreadsheetImportFields } from '@/spreadsheet-import/types'; -export const getFieldOptions = ( - fields: SpreadsheetImportFields, +export const getFieldOptions = ( + fields: SpreadsheetImportFields, fieldKey: string, ) => { const field = fields.find(({ key }) => fieldKey === key); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumns.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumns.ts deleted file mode 100644 index b52c0ae1e..000000000 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumns.ts +++ /dev/null @@ -1,52 +0,0 @@ -import lavenstein from 'js-levenshtein'; - -import { MatchColumnsStepProps } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; - -import { - SpreadsheetImportField, - SpreadsheetImportFields, -} from '@/spreadsheet-import/types'; -import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn'; -import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns'; -import { isDefined } from 'twenty-shared/utils'; -import { findMatch } from './findMatch'; -import { setColumn } from './setColumn'; - -export const getMatchedColumns = ( - columns: SpreadsheetColumns, - fields: SpreadsheetImportFields, - data: MatchColumnsStepProps['data'], - autoMapDistance: number, -) => - columns.reduce[]>((arr, column) => { - const autoMatch = findMatch(column.header, fields, autoMapDistance); - if (isDefined(autoMatch)) { - const field = fields.find( - (field) => field.key === autoMatch, - ) as SpreadsheetImportField; - const duplicateIndex = arr.findIndex( - (column) => 'value' in column && column.value === field.key, - ); - const duplicate = arr[duplicateIndex]; - if (duplicate && 'value' in duplicate) { - return lavenstein(duplicate.value, duplicate.header) < - lavenstein(autoMatch, column.header) - ? [ - ...arr.slice(0, duplicateIndex), - setColumn(arr[duplicateIndex], field, data), - ...arr.slice(duplicateIndex + 1), - setColumn(column), - ] - : [ - ...arr.slice(0, duplicateIndex), - setColumn(arr[duplicateIndex]), - ...arr.slice(duplicateIndex + 1), - setColumn(column, field, data), - ]; - } else { - return [...arr, setColumn(column, field, data)]; - } - } else { - return [...arr, column]; - } - }, []); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumnsWithFuse.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumnsWithFuse.ts index 0a3bab326..0383ff76e 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumnsWithFuse.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumnsWithFuse.ts @@ -11,16 +11,16 @@ import { setColumn } from '@/spreadsheet-import/utils/setColumn'; import Fuse from 'fuse.js'; import { isDefined } from 'twenty-shared/utils'; -export const getMatchedColumnsWithFuse = ({ +export const getMatchedColumnsWithFuse = ({ columns, fields, data, }: { - columns: SpreadsheetColumns; - fields: SpreadsheetImportFields; + columns: SpreadsheetColumns; + fields: SpreadsheetImportFields; data: MatchColumnsStepProps['data']; }) => { - const matchedColumns: SpreadsheetColumn[] = []; + const matchedColumns: SpreadsheetColumn[] = []; const fieldsToSearch = new Fuse(fields, { keys: ['label'], @@ -30,8 +30,8 @@ export const getMatchedColumnsWithFuse = ({ }); const suggestedFieldsByColumnHeader: Record< - SpreadsheetColumn['header'], - SpreadsheetImportField[] + SpreadsheetColumn['header'], + SpreadsheetImportField[] > = {}; for (const column of columns) { @@ -58,7 +58,7 @@ export const getMatchedColumnsWithFuse = ({ ); suggestedFieldsByColumnHeader[column.header] = fieldsThatMatch.map( - (match) => match.item as SpreadsheetImportField, + (match) => match.item as SpreadsheetImportField, ); if (isFirstMatchValid && isFieldStillUnmatched) { diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/getShortNestedFieldLabel.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/getShortNestedFieldLabel.ts new file mode 100644 index 000000000..7305ba763 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/getShortNestedFieldLabel.ts @@ -0,0 +1,3 @@ +export const getShortNestedFieldLabel = (label: string) => { + return label.split(' / ').slice(1).join(' / '); +}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/normalizeTableData.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/normalizeTableData.ts index 368fe71f3..d0fcc89d6 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/normalizeTableData.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/normalizeTableData.ts @@ -9,10 +9,10 @@ import { isDefined } from 'twenty-shared/utils'; import { z } from 'zod'; import { normalizeCheckboxValue } from './normalizeCheckboxValue'; -export const normalizeTableData = ( - columns: SpreadsheetColumns, +export const normalizeTableData = ( + columns: SpreadsheetColumns, data: ImportedRow[], - fields: SpreadsheetImportFields, + fields: SpreadsheetImportFields, ) => data.map((row) => columns.reduce((acc, column, index) => { @@ -101,5 +101,5 @@ export const normalizeTableData = ( default: return acc; } - }, {} as ImportedStructuredRow), + }, {} as ImportedStructuredRow), ); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/setColumn.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/setColumn.ts index c2cfd802c..98bcb2b79 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/setColumn.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/setColumn.ts @@ -9,17 +9,17 @@ import { isDefined } from 'twenty-shared/utils'; import { z } from 'zod'; import { uniqueEntries } from './uniqueEntries'; -export const setColumn = ( - oldColumn: SpreadsheetColumn, - field?: SpreadsheetImportField, +export const setColumn = ( + oldColumn: SpreadsheetColumn, + field?: SpreadsheetImportField, data?: MatchColumnsStepProps['data'], -): SpreadsheetColumn => { +): SpreadsheetColumn => { if (field?.fieldType.type === 'select') { const fieldOptions = field.fieldType.options; const uniqueData = uniqueEntries( data || [], oldColumn.index, - ) as SpreadsheetMatchedOptions[]; + ) as SpreadsheetMatchedOptions[]; const matchedOptions = uniqueData.map((record) => { const value = fieldOptions.find( @@ -28,8 +28,8 @@ export const setColumn = ( fieldOption.label === record.entry, )?.value; return value - ? ({ ...record, value } as SpreadsheetMatchedOptions) - : (record as SpreadsheetMatchedOptions); + ? ({ ...record, value } as SpreadsheetMatchedOptions) + : (record as SpreadsheetMatchedOptions); }); const allMatched = matchedOptions.filter((o) => o.value).length === uniqueData?.length; @@ -77,8 +77,8 @@ export const setColumn = ( fieldOption.value === entry || fieldOption.label === entry, )?.value; return value - ? ({ entry, value } as SpreadsheetMatchedOptions) - : ({ entry } as SpreadsheetMatchedOptions); + ? ({ entry, value } as SpreadsheetMatchedOptions) + : ({ entry } as SpreadsheetMatchedOptions); }); const areAllMatched = matchedOptions.filter((option) => option.value).length === diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/setIgnoreColumn.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/setIgnoreColumn.ts index 7123369cd..9d30c7684 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/setIgnoreColumn.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/setIgnoreColumn.ts @@ -1,10 +1,10 @@ import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn'; import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType'; -export const setIgnoreColumn = ({ +export const setIgnoreColumn = ({ header, index, -}: SpreadsheetColumn): SpreadsheetColumn => ({ +}: SpreadsheetColumn): SpreadsheetColumn => ({ header, index, type: SpreadsheetColumnType.ignored, diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/setSubColumn.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/setSubColumn.ts index 5bd63010d..a4ca89461 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/setSubColumn.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/setSubColumn.ts @@ -5,15 +5,13 @@ import { import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType'; import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions'; -export const setSubColumn = ( +export const setSubColumn = ( oldColumn: - | SpreadsheetMatchedSelectColumn - | SpreadsheetMatchedSelectOptionsColumn, + | SpreadsheetMatchedSelectColumn + | SpreadsheetMatchedSelectOptionsColumn, entry: string, value: string, -): - | SpreadsheetMatchedSelectColumn - | SpreadsheetMatchedSelectOptionsColumn => { +): SpreadsheetMatchedSelectColumn | SpreadsheetMatchedSelectOptionsColumn => { const shouldUnselectValue = oldColumn.matchedOptions.find((option) => option.entry === entry)?.value === value; @@ -28,13 +26,13 @@ export const setSubColumn = ( if (allMatched) { return { ...oldColumn, - matchedOptions: options as SpreadsheetMatchedOptions[], + matchedOptions: options as SpreadsheetMatchedOptions[], type: SpreadsheetColumnType.matchedSelectOptions, }; } else { return { ...oldColumn, - matchedOptions: options as SpreadsheetMatchedOptions[], + matchedOptions: options as SpreadsheetMatchedOptions[], type: SpreadsheetColumnType.matchedSelect, }; } diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetBuildFieldOptions.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetBuildFieldOptions.ts deleted file mode 100644 index 359eb7910..000000000 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetBuildFieldOptions.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { getFieldMetadataTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFieldMetadataTypeLabel'; -import { SpreadsheetImportFields } from '@/spreadsheet-import/types'; -import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns'; -import { FieldMetadataType } from 'twenty-shared/types'; - -export const spreadsheetBuildFieldOptions = ( - fields: SpreadsheetImportFields, - columns: SpreadsheetColumns, -) => { - return fields - .filter((field) => field.fieldMetadataType !== FieldMetadataType.RICH_TEXT) - .map(({ Icon, label, key, fieldMetadataType }) => { - const isSelected = - columns.findIndex((column) => { - if ('value' in column) { - return column.value === key; - } - return false; - }) !== -1; - - return { - Icon: Icon, - value: key, - label: label, - disabled: isSelected, - fieldMetadataTypeLabel: getFieldMetadataTypeLabel(fieldMetadataType), - } as const; - }); -}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportBuildFieldOptions.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportBuildFieldOptions.ts new file mode 100644 index 000000000..b3f649f3a --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportBuildFieldOptions.ts @@ -0,0 +1,42 @@ +import { getFieldMetadataTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFieldMetadataTypeLabel'; +import { SpreadsheetImportFields } from '@/spreadsheet-import/types'; +import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns'; +import { SpreadsheetImportFieldOption } from '@/spreadsheet-import/types/SpreadsheetImportFieldOption'; +import { getShortNestedFieldLabel } from '@/spreadsheet-import/utils/getShortNestedFieldLabel'; +import { ReadonlyDeep } from 'type-fest'; + +export const spreadsheetImportBuildFieldOptions = ( + fields: SpreadsheetImportFields, + columns: SpreadsheetColumns, +): readonly ReadonlyDeep[] => { + return fields.map( + ({ + Icon, + label, + key, + fieldMetadataType, + isNestedField, + fieldMetadataItemId, + }) => { + const isSelected = columns.some((column) => { + if ('value' in column) { + return column.value === key; + } + return false; + }); + + return { + Icon: Icon, + value: key, + label, + shortLabelForNestedField: isNestedField + ? getShortNestedFieldLabel(label) + : undefined, + disabled: isSelected, + fieldMetadataTypeLabel: getFieldMetadataTypeLabel(fieldMetadataType), + isNestedField: isNestedField, + fieldMetadataItemId: fieldMetadataItemId, + }; + }, + ); +}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportGetSubFieldOptions.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportGetSubFieldOptions.ts new file mode 100644 index 000000000..00f483a5e --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportGetSubFieldOptions.ts @@ -0,0 +1,14 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { SpreadsheetImportFieldOption } from '@/spreadsheet-import/types/SpreadsheetImportFieldOption'; + +export const getSubFieldOptions = ( + fieldMetadataItem: FieldMetadataItem, + options: readonly Readonly[], + searchFilter: string, +): readonly Readonly[] => { + return options.filter( + (option) => + option.fieldMetadataItemId === fieldMetadataItem.id && + option.label.toLowerCase().includes(searchFilter.toLowerCase()), + ); +}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportHasNestedFields.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportHasNestedFields.ts new file mode 100644 index 000000000..20025455e --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportHasNestedFields.ts @@ -0,0 +1,12 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationType } from '~/generated/graphql'; + +export const hasNestedFields = (fieldMetadata: FieldMetadataItem) => { + return ( + (fieldMetadata.type === FieldMetadataType.RELATION && + fieldMetadata.relation?.type === RelationType.MANY_TO_ONE) || + isCompositeFieldType(fieldMetadata.type) + ); +}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/uniqueEntries.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/uniqueEntries.ts index 16d825de4..32e079ba9 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/uniqueEntries.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/uniqueEntries.ts @@ -3,10 +3,10 @@ import uniqBy from 'lodash.uniqby'; import { MatchColumnsStepProps } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions'; -export const uniqueEntries = ( +export const uniqueEntries = ( data: MatchColumnsStepProps['data'], index: number, -): Partial>[] => +): Partial[] => uniqBy( data.map((row) => ({ entry: row[index] })), 'entry', diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-connect-input-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-connect-input-type-definition.factory.ts index a5325802d..3489b2c39 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-connect-input-type-definition.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-connect-input-type-definition.factory.ts @@ -8,6 +8,7 @@ import { GraphQLString, } from 'graphql'; import { RELATION_NESTED_QUERY_KEYWORDS } from 'twenty-shared/constants'; +import { getUniqueConstraintsFields } from 'twenty-shared/utils'; import { InputTypeDefinition, @@ -17,7 +18,6 @@ import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-build import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { getUniqueConstraintsFields } from 'src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { pascalCase } from 'src/utils/pascal-case'; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type.ts index 144abcec1..cc392f938 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type.ts @@ -10,14 +10,14 @@ export const fullNameCompositeType: CompositeType = { type: FieldMetadataType.TEXT, hidden: false, isRequired: false, - isIncludedInUniqueConstraint: true, + isIncludedInUniqueConstraint: false, }, { name: 'lastName', type: FieldMetadataType.TEXT, hidden: false, isRequired: false, - isIncludedInUniqueConstraint: true, + isIncludedInUniqueConstraint: false, }, ], }; diff --git a/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/relation-nested-queries.ts b/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/relation-nested-queries.ts index f2d988ea8..1a5ab87c7 100644 --- a/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/relation-nested-queries.ts +++ b/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/relation-nested-queries.ts @@ -15,6 +15,7 @@ import { TwentyORMException, TwentyORMExceptionCode, } from 'src/engine/twenty-orm/exceptions/twenty-orm.exception'; +import { formatConnectRecordNotFoundErrorMessage } from 'src/engine/twenty-orm/relation-nested-queries/utils/formatConnectRecordNotFoundErrorMessage.util'; import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; import { computeRelationConnectQueryConfigs } from 'src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util'; import { createSqlWhereTupleInClause } from 'src/engine/twenty-orm/utils/create-sql-where-tuple-in-clause.utils'; @@ -196,12 +197,19 @@ export class RelationNestedQueries { ); if (recordToConnect.length !== 1) { - const recordToConnectTotal = recordToConnect.length; - const connectFieldName = connectQueryConfig.connectFieldName; + const { errorMessage, userFriendlyMessage } = + formatConnectRecordNotFoundErrorMessage( + connectQueryConfig.connectFieldName, + recordToConnect.length, + connectQueryConfig.recordToConnectConditionByEntityIndex[index], + ); throw new TwentyORMException( - `Expected 1 record to connect to ${connectFieldName}, but found ${recordToConnectTotal}.`, + errorMessage, TwentyORMExceptionCode.CONNECT_RECORD_NOT_FOUND, + { + userFriendlyMessage, + }, ); } diff --git a/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/utils/__tests__/formatConnectRecordNotFoundErrorMessage.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/utils/__tests__/formatConnectRecordNotFoundErrorMessage.util.spec.ts new file mode 100644 index 000000000..5d7463d95 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/utils/__tests__/formatConnectRecordNotFoundErrorMessage.util.spec.ts @@ -0,0 +1,21 @@ +import { formatConnectRecordNotFoundErrorMessage } from 'src/engine/twenty-orm/relation-nested-queries/utils/formatConnectRecordNotFoundErrorMessage.util'; + +describe('formatConnectRecordNotFoundErrorMessage', () => { + it('should format the error message correctly', () => { + const result = formatConnectRecordNotFoundErrorMessage( + 'connectFieldName', + 0, + [ + ['field1', 'value1'], + ['field2', 'value2'], + ], + ); + + expect(result).toEqual({ + errorMessage: + 'Expected 1 record to connect to connectFieldName, but found 0 for field1 = value1 and field2 = value2', + userFriendlyMessage: + 'Expected 1 record to connect to connectFieldName, but found 0 for field1 = value1 and field2 = value2', + }); + }); +}); diff --git a/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/utils/formatConnectRecordNotFoundErrorMessage.util.ts b/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/utils/formatConnectRecordNotFoundErrorMessage.util.ts new file mode 100644 index 000000000..d5d87df10 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/utils/formatConnectRecordNotFoundErrorMessage.util.ts @@ -0,0 +1,18 @@ +import { t } from '@lingui/core/macro'; + +import { UniqueConstraintCondition } from 'src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type'; + +export const formatConnectRecordNotFoundErrorMessage = ( + connectFieldName: string, + recordToConnectTotal: number, + uniqueConstraint: UniqueConstraintCondition, +) => { + const formattedConnectCondition = uniqueConstraint + .map(([field, value]) => `${field} = ${value}`) + .join(' and '); + + return { + errorMessage: `Expected 1 record to connect to ${connectFieldName}, but found ${recordToConnectTotal} for ${formattedConnectCondition}`, + userFriendlyMessage: t`Expected 1 record to connect to ${connectFieldName}, but found ${recordToConnectTotal} for ${formattedConnectCondition}`, + }; +}; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts index 9e3611558..5c7ba108c 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts @@ -1,13 +1,12 @@ import { t } from '@lingui/core/macro'; import deepEqual from 'deep-equal'; import { FieldMetadataType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { getUniqueConstraintsFields, isDefined } from 'twenty-shared/utils'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { getUniqueConstraintsFields } from 'src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/nested-relation-queries.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/nested-relation-queries.integration-spec.ts index 141f59e4c..db17f0bd0 100644 --- a/packages/twenty-server/test/integration/graphql/suites/object-generated/nested-relation-queries.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/nested-relation-queries.integration-spec.ts @@ -336,7 +336,7 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)', expect(response.body.errors).toBeDefined(); expect(response.body.errors[0].message).toBe( - 'Expected 1 record to connect to company, but found 0.', + 'Expected 1 record to connect to company, but found 0 for domainNamePrimaryLinkUrl = not-existing-company', ); expect(response.body.errors[0].extensions.code).toBe( ErrorCode.BAD_USER_INPUT, diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts index 0ba5dad80..8c4b1c4f0 100644 --- a/packages/twenty-shared/src/utils/index.ts +++ b/packages/twenty-shared/src/utils/index.ts @@ -15,6 +15,7 @@ export { sanitizeURL, getLogoUrlFromDomainName, } from './image/getLogoUrlFromDomainName'; +export { getUniqueConstraintsFields } from './indexMetadata/getUniqueConstraintsFields'; export { parseJson } from './parseJson'; export { removeUndefinedFields } from './removeUndefinedFields'; export { getGenericOperationName } from './sentry/getGenericOperationName'; diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/__tests__/getUniqueConstraintsFields.util.spec.ts b/packages/twenty-shared/src/utils/indexMetadata/__tests__/getUniqueConstraintsFields.test.ts similarity index 94% rename from packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/__tests__/getUniqueConstraintsFields.util.spec.ts rename to packages/twenty-shared/src/utils/indexMetadata/__tests__/getUniqueConstraintsFields.test.ts index 1875ab8f9..7b5ed78a8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/__tests__/getUniqueConstraintsFields.util.spec.ts +++ b/packages/twenty-shared/src/utils/indexMetadata/__tests__/getUniqueConstraintsFields.test.ts @@ -1,6 +1,5 @@ -import { FieldMetadataType } from 'twenty-shared/types'; - -import { getUniqueConstraintsFields } from 'src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util'; +import { FieldMetadataType } from '@/types'; +import { getUniqueConstraintsFields } from '@/utils/indexMetadata/getUniqueConstraintsFields'; describe('getUniqueConstraintsFields', () => { const mockIdField = { diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util.ts b/packages/twenty-shared/src/utils/indexMetadata/getUniqueConstraintsFields.ts similarity index 95% rename from packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util.ts rename to packages/twenty-shared/src/utils/indexMetadata/getUniqueConstraintsFields.ts index be70d2b47..b72e0ddee 100644 --- a/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util.ts +++ b/packages/twenty-shared/src/utils/indexMetadata/getUniqueConstraintsFields.ts @@ -1,4 +1,4 @@ -import { isDefined } from 'twenty-shared/utils'; +import { isDefined } from '@/utils/validation/isDefined'; export const getUniqueConstraintsFields = < K extends { diff --git a/packages/twenty-shared/src/utils/indexMetadata/index.ts b/packages/twenty-shared/src/utils/indexMetadata/index.ts new file mode 100644 index 000000000..96de704c1 --- /dev/null +++ b/packages/twenty-shared/src/utils/indexMetadata/index.ts @@ -0,0 +1 @@ +export * from './getUniqueConstraintsFields';