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 }) => (
-
);
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