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
deleted file mode 100644
index def0d25dc..000000000
--- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useBuildSpreadSheetImportFields.test.tsx
+++ /dev/null
@@ -1,503 +0,0 @@
-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 8c9d3bfa2..6a345c09f 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,13 +383,9 @@ describe('useSpreadsheetCompanyImport', () => {
expect(spreadsheetImportDialogAfterOpen.options?.onSubmit).toBeInstanceOf(
Function,
);
- expect(spreadsheetImportDialogAfterOpen.options).toHaveProperty(
- 'spreadsheetImportFields',
- );
+ expect(spreadsheetImportDialogAfterOpen.options).toHaveProperty('fields');
expect(
- Array.isArray(
- spreadsheetImportDialogAfterOpen.options?.spreadsheetImportFields,
- ),
+ Array.isArray(spreadsheetImportDialogAfterOpen.options?.fields),
).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
new file mode 100644
index 000000000..135176e76
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts
@@ -0,0 +1,177 @@
+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
deleted file mode 100644
index 428daaf21..000000000
--- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildSpreadSheetImportFields.ts
+++ /dev/null
@@ -1,284 +0,0 @@
-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, {
- 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 331eee1b4..6e255ba86 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 { useBuildSpreadsheetImportFields } from '@/object-record/spreadsheet-import/hooks/useBuildSpreadSheetImportFields';
+import { useBuildAvailableFieldsForImport } from '@/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport';
import { buildRecordFromImportedStructuredRow } from '@/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow';
-import { spreadsheetImportFilterAvailableFieldMetadataItems } from '@/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems';
+import { spreadsheetImportFilterAvailableFieldMetadataItems } from '@/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts';
import { spreadsheetImportGetUnicityRowHook } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetUnicityRowHook';
import { SpreadsheetImportCreateRecordsBatchSize } from '@/spreadsheet-import/constants/SpreadsheetImportCreateRecordsBatchSize';
import { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog';
@@ -10,13 +10,12 @@ 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 { buildSpreadsheetImportFields } = useBuildSpreadsheetImportFields();
-
+ const { openSpreadsheetImportDialog } = useOpenSpreadsheetImportDialog();
const { enqueueErrorSnackBar } = useSnackBar();
const { objectMetadataItem } = useObjectMetadataItem({
@@ -36,19 +35,28 @@ 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 spreadsheetImportFields = buildSpreadsheetImportFields(
- availableFieldMetadataItemsToImport,
+ const availableFieldMetadataItemsForMatching =
+ availableFieldMetadataItemsToImport.filter(
+ (fieldMetadataItem) =>
+ fieldMetadataItem.type !== FieldMetadataType.ACTOR,
+ );
+
+ const availableFieldsForMatching = buildAvailableFieldsForImport(
+ availableFieldMetadataItemsForMatching,
);
openSpreadsheetImportDialog({
@@ -58,8 +66,7 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
const fieldMapping: Record =
buildRecordFromImportedStructuredRow({
importedStructuredRow: record,
- fieldMetadataItems: availableFieldMetadataItemsToImport,
- spreadsheetImportFields,
+ fields: availableFieldMetadataItemsToImport,
});
return fieldMapping;
@@ -76,7 +83,7 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
});
}
},
- spreadsheetImportFields,
+ fields: availableFieldsForMatching,
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
new file mode 100644
index 000000000..1a8b46a62
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/types/AvailableFieldForImport.ts
@@ -0,0 +1,15 @@
+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 ed663b5fe..82099dd47 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,20 +1,15 @@
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,
- SpreadsheetImportField,
-} from '@/spreadsheet-import/types';
+import { ImportedStructuredRow } 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"]',
- 'nameField (relationField)': 'John Doe',
+ relationField: 'company-123',
selectField: 'option1',
arrayField: '["item1", "item2", "item3"]',
jsonField: '{"key": "value", "nested": {"prop": "data"}}',
@@ -127,9 +122,6 @@ describe('buildRecordFromImportedStructuredRow', () => {
updatedAt: '2023-01-01',
icon: 'IconBuilding',
description: null,
- relation: {
- type: RelationType.MANY_TO_ONE,
- } as FieldMetadataItemRelation,
},
{
id: '7',
@@ -345,25 +337,9 @@ 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,
- fieldMetadataItems: fields,
- spreadsheetImportFields,
+ fields,
});
expect(result).toEqual({
@@ -374,14 +350,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
booleanField: true,
numberField: 30,
multiSelectField: ['tag1', 'tag2', 'tag3'],
- relationField: {
- connect: {
- where: {
- nameField: 'John Doe',
- },
- },
- },
- relationFieldId: undefined,
+ relationFieldId: 'company-123',
selectField: 'option1',
arrayField: ['item1', 'item2', 'item3'],
jsonField: { key: 'value', nested: { prop: 'data' } },
@@ -437,8 +406,8 @@ describe('buildRecordFromImportedStructuredRow', () => {
});
});
- it('should successfully build a record from imported structured row with primary phone number (without calling code)', () => {
- const importedStructuredRow: ImportedStructuredRow = {
+ it('should handle case where user provides only a primaryPhoneNumber without calling code', () => {
+ const importedStructuredRow: ImportedStructuredRow = {
'Primary Phone Number (phoneField)': '5550123',
};
@@ -461,8 +430,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
const result = buildRecordFromImportedStructuredRow({
importedStructuredRow,
- fieldMetadataItems: fields,
- spreadsheetImportFields: [],
+ fields,
});
expect(result).toEqual({
@@ -472,64 +440,4 @@ 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 498602b37..3575d398a 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 604b88f98..6d5a160fc 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,38 +1,31 @@
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 { getCompositeSubFieldKey } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldKey';
-import {
- ImportedStructuredRow,
- SpreadsheetImportFields,
-} from '@/spreadsheet-import/types';
+import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey';
+import { ImportedStructuredRow } from '@/spreadsheet-import/types';
import { isNonEmptyString } from '@sniptt/guards';
import { CountryCode, parsePhoneNumberWithError } from 'libphonenumber-js';
-import { assertUnreachable, isDefined } from 'twenty-shared/utils';
+import { isDefined } from 'twenty-shared/utils';
import { z } from 'zod';
-import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
+import { FieldMetadataType } 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;
- fieldMetadataItems: FieldMetadataItem[];
- spreadsheetImportFields: SpreadsheetImportFields;
+ importedStructuredRow: ImportedStructuredRow;
+ fields: FieldMetadataItem[];
};
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[
- getCompositeSubFieldKey(field, compositeFieldKey)
- ];
+ importedStructuredRow[getSubFieldOptionKey(field, compositeFieldKey)];
return isDefined(value)
? { ...acc, [compositeFieldKey]: transform?.(value) || value }
@@ -44,59 +37,9 @@ 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 = ({
- fieldMetadataItems,
+ fields,
importedStructuredRow,
- spreadsheetImportFields,
}: BuildRecordFromImportedStructuredRowArgs) => {
const stringArrayJSONSchema = z
.preprocess((value) => {
@@ -202,7 +145,7 @@ export const buildRecordFromImportedStructuredRow = ({
},
};
- for (const field of fieldMetadataItems) {
+ for (const field of fields) {
const importedFieldValue = importedStructuredRow[field.name];
switch (field.type) {
@@ -235,12 +178,12 @@ export const buildRecordFromImportedStructuredRow = ({
const primaryPhoneNumber =
importedStructuredRow[
- getCompositeSubFieldKey(field, 'primaryPhoneNumber')
+ getSubFieldOptionKey(field, 'primaryPhoneNumber')
];
const primaryPhoneCallingCode =
importedStructuredRow[
- getCompositeSubFieldKey(field, 'primaryPhoneCallingCode')
+ getSubFieldOptionKey(field, 'primaryPhoneCallingCode')
];
const hasUserProvidedPrimaryPhoneNumberWithoutCallingCode =
@@ -252,7 +195,7 @@ export const buildRecordFromImportedStructuredRow = ({
if (hasUserProvidedPrimaryPhoneNumberWithoutCallingCode) {
const primaryPhoneCountryCode =
importedStructuredRow[
- getCompositeSubFieldKey(field, 'primaryPhoneCountryCode')
+ getSubFieldOptionKey(field, 'primaryPhoneCountryCode')
];
const hasUserProvidedPrimaryPhoneCountryCode =
@@ -294,14 +237,22 @@ export const buildRecordFromImportedStructuredRow = ({
case FieldMetadataType.NUMERIC:
recordToBuild[field.name] = Number(importedFieldValue);
break;
- case FieldMetadataType.RELATION: {
- recordToBuild[field.name] = buildRelationConnectFieldRecord(
- field,
- importedStructuredRow,
- spreadsheetImportFields,
- );
+ 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;
+
break;
- }
case FieldMetadataType.ACTOR:
recordToBuild[field.name] = {
source: 'IMPORT',
@@ -324,30 +275,11 @@ export const buildRecordFromImportedStructuredRow = ({
}
break;
}
- 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:
+ default:
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/spreadsheetImportGetCompositeSubFieldKey.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSubFieldOptionKey.ts
similarity index 69%
rename from packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldKey.ts
rename to packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSubFieldOptionKey.ts
index 9cfe6df6a..b9f0d78d5 100644
--- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldKey.ts
+++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSubFieldOptionKey.ts
@@ -2,18 +2,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';
-export const getCompositeSubFieldKey = (
+export const getSubFieldOptionKey = (
fieldMetadataItem: FieldMetadataItem,
subFieldName: string,
) => {
if (!isCompositeFieldType(fieldMetadataItem.type)) {
throw new Error(
- `getCompositeSubFieldKey can only be called for composite field types. Received: ${fieldMetadataItem.type}`,
+ `getSubFieldOptionKey can only be called for composite field types. Received: ${fieldMetadataItem.type}`,
);
}
const subFieldLabel =
COMPOSITE_FIELD_SUB_FIELD_LABELS[fieldMetadataItem.type][subFieldName];
- return `${subFieldLabel} (${fieldMetadataItem.name})`;
+ const subFieldKey = `${subFieldLabel} (${fieldMetadataItem.name})`;
+
+ return subFieldKey;
};
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
deleted file mode 100644
index 369328d7d..000000000
--- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadSheetGetRelationConnectSubFieldKey.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-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 b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts.ts
similarity index 100%
rename from packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts
rename to packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts.ts
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
deleted file mode 100644
index da5ae43ed..000000000
--- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldLabelWithFieldLabel.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-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
deleted file mode 100644
index cd75f066b..000000000
--- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetRelationConnectSubFieldLabel.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-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 8d17f2eca..52343e47d 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,7 +1,6 @@
-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 { getCompositeSubFieldKey } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldKey';
+import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey';
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 {
@@ -12,7 +11,6 @@ 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';
@@ -25,14 +23,22 @@ type Column = {
export const spreadsheetImportGetUnicityRowHook = (
objectMetadataItem: ObjectMetadataItem,
) => {
- const uniqueConstraintsFields = getUniqueConstraintsFields<
- FieldMetadataItem,
- 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 uniqueConstraintsWithColumnNames: Column[][] =
- uniqueConstraintsFields.map((uniqueConstraintFields) =>
- uniqueConstraintFields.flatMap((field) => {
if (isCompositeFieldType(field.type)) {
const compositeTypeFieldConfig =
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[field.type];
@@ -42,16 +48,18 @@ export const spreadsheetImportGetUnicityRowHook = (
);
return uniqueSubFields.map((subField) => ({
- columnName: getCompositeSubFieldKey(field, subField.subFieldName),
+ columnName: getSubFieldOptionKey(field, subField.subFieldName),
fieldType: field.type,
}));
}
return [{ columnName: field.name, fieldType: field.type }];
}),
- );
- const rowHook: SpreadsheetImportRowHook = (row, addError, table) => {
- if (uniqueConstraintsFields.length === 0) {
+ ),
+ ];
+
+ const rowHook: SpreadsheetImportRowHook = (row, addError, table) => {
+ if (uniqueConstraints.length === 0) {
return row;
}
@@ -87,7 +95,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 f42ed7033..7cfd395c2 100644
--- a/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts
+++ b/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts
@@ -42,10 +42,18 @@ export const sanitizeRecordInput = ({
if (
isDefined(fieldMetadataItem) &&
fieldMetadataItem.type === FieldMetadataType.RELATION &&
- fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE &&
- !isDefined(recordInput[fieldMetadataItem.name]?.connect?.where)
+ fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE
) {
- return undefined;
+ const relationIdFieldName = `${fieldMetadataItem.name}Id`;
+ const relationIdFieldMetadataItem = objectMetadataItem.fields.find(
+ (field) => field.name === relationIdFieldName,
+ );
+
+ const relationIdFieldValue = recordInput[relationIdFieldName];
+
+ return relationIdFieldMetadataItem
+ ? [relationIdFieldName, relationIdFieldValue ?? null]
+ : 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 b7a489afd..44852d244 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
-export type CompositeSubFieldConfig = {
+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: false,
+ isIncludedInUniqueConstraint: true,
},
{
subFieldName: 'lastName',
@@ -267,7 +267,7 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
.lastName,
isImportable: true,
isFilterable: true,
- isIncludedInUniqueConstraint: false,
+ isIncludedInUniqueConstraint: true,
},
],
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 346942c28..3500e7437 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,6 +16,7 @@ const fields = [
fieldType: {
type: 'input',
},
+ example: 'Stephanie',
fieldValidationDefinitions: [
{
rule: 'required',
@@ -23,8 +24,6 @@ const fields = [
},
],
fieldMetadataType: FieldMetadataType.TEXT,
- fieldMetadataItemId: '1',
- isNestedField: false,
},
{
Icon: null,
@@ -34,6 +33,7 @@ const fields = [
fieldType: {
type: 'input',
},
+ example: 'McDonald',
fieldValidationDefinitions: [
{
rule: 'unique',
@@ -42,9 +42,6 @@ const fields = [
},
],
description: 'Family / Last name',
- fieldMetadataType: FieldMetadataType.TEXT,
- fieldMetadataItemId: '2',
- isNestedField: false,
},
{
Icon: null,
@@ -54,6 +51,7 @@ const fields = [
fieldType: {
type: 'input',
},
+ example: '23',
fieldValidationDefinitions: [
{
rule: 'regex',
@@ -62,14 +60,12 @@ const fields = [
level: 'warning',
},
],
- fieldMetadataType: FieldMetadataType.TEXT,
- fieldMetadataItemId: '3',
- isNestedField: false,
},
{
Icon: null,
label: 'Team',
key: 'team',
+ alternateMatches: ['department'],
fieldType: {
type: 'select',
options: [
@@ -77,31 +73,28 @@ 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: {},
},
- fieldMetadataType: FieldMetadataType.TEXT,
- fieldMetadataItemId: '5',
- isNestedField: false,
+ example: 'true',
},
-] as SpreadsheetImportFields;
+] as SpreadsheetImportFields;
-export const importedColums: SpreadsheetColumns = [
+export const importedColums: SpreadsheetColumns = [
{
header: 'Name',
index: 0,
@@ -128,13 +121,13 @@ export const importedColums: SpreadsheetColumns = [
},
];
-const mockComponentBehaviourForTypes = (
- props: SpreadsheetImportDialogOptions,
+const mockComponentBehaviourForTypes = (
+ props: SpreadsheetImportDialogOptions,
) => props;
export const mockRsiValues = mockComponentBehaviourForTypes({
...defaultSpreadsheetImportProps,
- spreadsheetImportFields: fields,
+ fields: 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 5538f797b..20fec1153 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={hasNestedFields(field)}
+ hasSubMenu={isCompositeFieldType(field.type)}
/>
))}
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 968a963c2..e63c0d792 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx
+++ b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx
@@ -1,7 +1,9 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
-import { SpreadsheetImportFieldOption } from '@/spreadsheet-import/types/SpreadsheetImportFieldOption';
-import { getSubFieldOptions } from '@/spreadsheet-import/utils/spreadsheetImportGetSubFieldOptions';
-import { hasNestedFields } from '@/spreadsheet-import/utils/spreadsheetImportHasNestedFields';
+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 { 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';
@@ -10,8 +12,15 @@ 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 { IconChevronLeft, OverflowingTextWithTooltip } from 'twenty-ui/display';
+import { isDefined } from 'twenty-shared/utils';
+import {
+ IconChevronLeft,
+ OverflowingTextWithTooltip,
+ useIcons,
+} from 'twenty-ui/display';
+import { SelectOption } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
+import { ReadonlyDeep } from 'type-fest';
export const MatchColumnSelectSubFieldSelectDropdownContent = ({
fieldMetadataItem,
@@ -21,11 +30,13 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({
}: {
fieldMetadataItem: FieldMetadataItem;
onSubFieldSelect: (subFieldNameSelected: string) => void;
- options: readonly Readonly[];
+ options: readonly ReadonlyDeep[];
onBack: () => void;
}) => {
const [searchFilter, setSearchFilter] = useState('');
+ const { getIcon } = useIcons();
+
const handleFilterChange = (event: React.ChangeEvent) => {
const value = event.currentTarget.value;
@@ -41,15 +52,31 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({
onBack();
};
- if (!hasNestedFields(fieldMetadataItem)) {
+ if (!isCompositeFieldType(fieldMetadataItem.type)) {
return <>>;
}
- const subFieldOptions = getSubFieldOptions(
- fieldMetadataItem,
- options,
- searchFilter,
- );
+ 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()),
+ );
return (
@@ -70,17 +97,24 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({
/>
- {subFieldOptions.map(
- ({ value, shortLabelForNestedField, Icon, disabled }) => (
-
);
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx
index 649cd29aa..32a8bdcae 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx
+++ b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx
@@ -3,11 +3,10 @@ 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';
@@ -20,7 +19,7 @@ interface MatchColumnToFieldSelectProps {
columnIndex: string;
onChange: (value: ReadonlyDeep | null) => void;
value?: ReadonlyDeep;
- options: readonly Readonly[];
+ options: readonly ReadonlyDeep[];
suggestedOptions: readonly ReadonlyDeep[];
placeholder?: string;
}
@@ -71,7 +70,12 @@ export const MatchColumnToFieldSelect = ({
}
const correspondingOption = options.find((option) => {
- return option.value === subFieldNameSelected;
+ const optionKey = getSubFieldOptionKey(
+ selectedFieldMetadataItem,
+ subFieldNameSelected,
+ );
+
+ return option.value === optionKey;
});
if (isDefined(correspondingOption)) {
@@ -108,9 +112,9 @@ export const MatchColumnToFieldSelect = ({
closeDropdown(dropdownId);
};
- const shouldShowNestedField =
+ const shouldShowSubField =
isDefined(selectedFieldMetadataItem) &&
- hasNestedFields(selectedFieldMetadataItem);
+ isCompositeFieldType(selectedFieldMetadataItem.type);
return (
}
dropdownComponents={
- shouldShowNestedField ? (
+ shouldShowSubField ? (
= {
children: React.ReactNode;
- values: SpreadsheetImportDialogOptions;
+ values: SpreadsheetImportDialogOptions;
};
-export const ReactSpreadsheetImportContextProvider = ({
+export const ReactSpreadsheetImportContextProvider = ({
children,
values,
-}: ReactSpreadsheetImportContextProviderProps) => {
- if (isUndefinedOrNull(values.spreadsheetImportFields)) {
+}: ReactSpreadsheetImportContextProviderProps) => {
+ if (isUndefinedOrNull(values.fields)) {
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 69f0495b6..e48fe1b38 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,43 +13,45 @@ import { act } from 'react';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
);
+type SpreadsheetKey = 'spreadsheet_key';
-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: [],
-};
+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: [],
+ };
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 7300a0a60..5640640da 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/hooks/useComputeColumnSuggestionsAndAutoMatch.ts
+++ b/packages/twenty-front/src/modules/spreadsheet-import/hooks/useComputeColumnSuggestionsAndAutoMatch.ts
@@ -8,9 +8,8 @@ import { ImportedRow } from '@/spreadsheet-import/types';
import { getMatchedColumnsWithFuse } from '@/spreadsheet-import/utils/getMatchedColumnsWithFuse';
import { useRecoilCallback } from 'recoil';
-export const useComputeColumnSuggestionsAndAutoMatch = () => {
- const { spreadsheetImportFields: fields, autoMapHeaders } =
- useSpreadsheetImportInternal();
+export const useComputeColumnSuggestionsAndAutoMatch = () => {
+ const { 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 3e1d06497..f68cc75cd 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,
+ options: Omit, 'isOpen' | 'onClose'>,
) => {
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 85ef96029..fd5aec6c3 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 a94169eec..ef265a29e 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,7 +10,9 @@ 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 = {
+export const defaultSpreadsheetImportProps: Partial<
+ SpreadsheetImportProps
+> = {
autoMapHeaders: true,
allowInvalidSubmit: true,
autoMapDistance: 2,
@@ -26,11 +28,13 @@ 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 cfec8c88a..cd0d5c904 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/states/spreadsheetImportDialogState.ts
+++ b/packages/twenty-front/src/modules/spreadsheet-import/states/spreadsheetImportDialogState.ts
@@ -1,18 +1,19 @@
import { createState } from 'twenty-ui/utilities';
import { SpreadsheetImportDialogOptions } from '../types';
-export type SpreadsheetImportDialogState = {
+export type SpreadsheetImportDialogState = {
isOpen: boolean;
isStepBarVisible: boolean;
- options: Omit | null;
+ options: Omit, 'isOpen' | 'onClose'> | null;
};
-export const spreadsheetImportDialogState =
- createState({
- key: 'spreadsheetImportDialogState',
- defaultValue: {
- isOpen: false,
- isStepBarVisible: true,
- options: null,
- },
- });
+export const spreadsheetImportDialogState = createState<
+ SpreadsheetImportDialogState
+>({
+ 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 bbcfc4b09..7ba45e779 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,6 +1,7 @@
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';
@@ -37,6 +38,9 @@ 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 f4bf87b71..3054a4308 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 { spreadsheetImportFields: fields } = useSpreadsheetImportInternal();
+ const { 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: string, columnIndex: number) => {
+ (value: T, 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 218f15baf..fdf4c3cd1 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 79f90ef46..527ef49a1 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 { spreadsheetImportFields: fields } = useSpreadsheetImportInternal();
+}: SubMatchingSelectDropdownButtonProps) => {
+ const { 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 7b823535e..3786c2733 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: string, index: number, option: string) => void;
+ | SpreadsheetMatchedSelectColumn
+ | SpreadsheetMatchedSelectOptionsColumn;
+ onSubChange: (val: T, 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 487bf0b04..0434cc777 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 4098cef19..69c52de5d 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: string, index: number, option: string) => void;
+ | SpreadsheetMatchedSelectColumn
+ | SpreadsheetMatchedSelectOptionsColumn;
+ onSubChange: (val: T, 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 { spreadsheetImportFields: fields } = useSpreadsheetImportInternal();
+ const { 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, column.index, option.entry ?? '');
+ onSubChange(selectedOption.value as T, 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 4094bd570..4bb36d6af 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 { spreadsheetImportBuildFieldOptions } from '@/spreadsheet-import/utils/spreadsheetImportBuildFieldOptions';
+import { spreadsheetBuildFieldOptions } from '@/spreadsheet-import/utils/spreadsheetBuildFieldOptions';
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: string, index: number) => void;
+ onChange: (val: T, index: number) => void;
};
-export const TemplateColumn = ({
+export const TemplateColumn = ({
columns,
columnIndex,
onChange,
-}: TemplateColumnProps) => {
- const { spreadsheetImportFields: fields } = useSpreadsheetImportInternal();
+}: TemplateColumnProps) => {
+ const { fields } = useSpreadsheetImportInternal();
const suggestedFieldsByColumnHeader = useRecoilValue(
suggestedFieldsByColumnHeaderState,
);
@@ -46,8 +46,8 @@ export const TemplateColumn = ({
const { t } = useLingui();
- const fieldOptions = spreadsheetImportBuildFieldOptions(fields, columns);
- const suggestedFieldOptions = spreadsheetImportBuildFieldOptions(
+ const fieldOptions = spreadsheetBuildFieldOptions(fields, columns);
+ const suggestedFieldOptions = spreadsheetBuildFieldOptions(
suggestedFieldsByColumnHeader[column.header] ?? [],
columns,
);
@@ -74,7 +74,7 @@ export const TemplateColumn = ({
onChange(value?.value as string, column.index)}
+ onChange={(value) => onChange(value?.value as T, 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 64699c6ea..55f4320d3 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: string, index: number, option: string) => void;
+ onSubChange: (val: T, 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 { spreadsheetImportFields: fields } = useSpreadsheetImportInternal();
+}: UnmatchColumnProps) => {
+ const { 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 410bb1d01..46215c495 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 51ade3b39..0698e795b 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 4efcbba29..8ba18b3ed 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
new file mode 100644
index 000000000..9ccedc314
--- /dev/null
+++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/columns.tsx
@@ -0,0 +1,58 @@
+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 256cc54f9..4c02ac94d 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,6 +1,5 @@
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
-import { spreadsheetImportFilterAvailableFieldMetadataItems } from '@/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems';
-import { getCompositeSubFieldLabelWithFieldLabel } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldLabelWithFieldLabel';
+import { spreadsheetImportFilterAvailableFieldMetadataItems } from '@/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts';
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';
@@ -59,8 +58,8 @@ export const useDownloadFakeRecords = () => {
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[field.type].exampleValues;
headerRow.push(
- ...subFields.map(({ subFieldLabel }) =>
- getCompositeSubFieldLabelWithFieldLabel(field, subFieldLabel),
+ ...subFields.map(
+ ({ subFieldLabel }) => `${field.label} / ${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 98a0cdead..f2971c7ea 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,6 +1,5 @@
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';
@@ -92,36 +91,30 @@ 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) => {
- const hideStepBar = useHideStepBar();
+}: ValidationStepProps) => {
const { enqueueDialog } = useDialogManager();
- const {
- spreadsheetImportFields: fields,
- onClose,
- onSubmit,
- rowHook,
- tableHook,
- } = useSpreadsheetImportInternal();
+ const { 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
[],
),
@@ -133,7 +126,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],
);
@@ -212,7 +205,8 @@ export const ValidationStep = ({
}, [data, filterByErrors]);
const rowKeyGetter = useCallback(
- (row: ImportedStructuredRow & ImportedStructuredRowMetadata) => row.__index,
+ (row: ImportedStructuredRow & ImportedStructuredRowMetadata) =>
+ row.__index,
[],
);
@@ -224,29 +218,28 @@ 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 f5aa86edb..8dc8379cd 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[] => [
+export const generateColumns = (
+ fields: SpreadsheetImportFields,
+): Column & ImportedStructuredRowMetadata>[] => [
{
key: SELECT_COLUMN_KEY,
name: '',
@@ -108,7 +108,7 @@ export const generateColumns = (
...fields.map(
(
column,
- ): Column => ({
+ ): Column & ImportedStructuredRowMetadata> => ({
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],
+ (option) => option.value === row[columnKey as T],
)?.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 6a405c864..22f2292c4 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 74b2204b4..74404b854 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: string;
+ value: T;
};
-type SpreadsheetMatchedSwitchColumn = {
+type SpreadsheetMatchedSwitchColumn = {
type: SpreadsheetColumnType.matchedCheckbox;
index: number;
header: string;
- value: string;
+ value: T;
};
-export type SpreadsheetMatchedSelectColumn = {
+export type SpreadsheetMatchedSelectColumn = {
type: SpreadsheetColumnType.matchedSelect;
index: number;
header: string;
- value: string;
- matchedOptions: Partial[];
+ value: T;
+ matchedOptions: Partial>[];
};
-export type SpreadsheetMatchedSelectOptionsColumn = {
+export type SpreadsheetMatchedSelectOptionsColumn = {
type: SpreadsheetColumnType.matchedSelectOptions;
index: number;
header: string;
- value: string;
- matchedOptions: SpreadsheetMatchedOptions[];
+ value: T;
+ matchedOptions: SpreadsheetMatchedOptions[];
};
-export type SpreadsheetErrorColumn = {
+export type SpreadsheetErrorColumn = {
type: SpreadsheetColumnType.matchedError;
index: number;
header: string;
- value: string;
+ value: T;
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 0e85854ca..e53cfb5e0 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 457bef90c..e308df5c6 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
- spreadsheetImportFields: SpreadsheetImportFields;
+ fields: 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,6 +59,5 @@ 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 320df13e9..02e8d43de 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportField.ts
+++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportField.ts
@@ -1,34 +1,25 @@
-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: string;
- // Field's metadata item id - same for all associated nested fields
- fieldMetadataItemId: string;
+ key: T;
// 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;
- // 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;
+ // UI-facing values shown to user as field examples pre-upload phase
+ example?: string;
};
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFieldOption.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFieldOption.ts
deleted file mode 100644
index 493398481..000000000
--- a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFieldOption.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-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 369b98e0b..8a1549867 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFields.ts
+++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFields.ts
@@ -1,4 +1,6 @@
import { SpreadsheetImportField } from '@/spreadsheet-import/types/SpreadsheetImportField';
import { ReadonlyDeep } from 'type-fest';
-export type SpreadsheetImportFields = ReadonlyDeep;
+export type SpreadsheetImportFields = ReadonlyDeep<
+ SpreadsheetImportField[]
+>;
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 378d58469..d2016772a 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportValidationResult.ts
+++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportValidationResult.ts
@@ -1,8 +1,9 @@
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 a62b02897..1c428117b 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: string]: string | boolean | undefined;
+export type ImportedStructuredRow = {
+ [key in T]: 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 1d81fa5e0..a3db443fe 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: string, error: SpreadsheetImportInfo) => void,
- table: ImportedStructuredRow[],
-) => ImportedStructuredRow;
+export type SpreadsheetImportRowHook = (
+ row: ImportedStructuredRow,
+ addError: (fieldKey: T, 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 17b44f134..7459de943 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: string,
+ fieldKey: T,
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 a7d124363..98beecf99 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?: string;
+ value?: T;
};
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 776622de6..5d62ad5bf 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,16 +9,17 @@ import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations';
import { FieldMetadataType } from 'twenty-shared/types';
describe('addErrorsAndRunHooks', () => {
- const requiredField = {
+ type FullData = ImportedStructuredRow<'name' | 'age' | 'country'>;
+ const requiredField: SpreadsheetImportField<'name'> = {
key: 'name',
label: 'Name',
fieldValidationDefinitions: [{ rule: 'required' }],
Icon: null,
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.TEXT,
- } as SpreadsheetImportField;
+ };
- const regexField = {
+ const regexField: SpreadsheetImportField<'age'> = {
key: 'age',
label: 'Age',
fieldValidationDefinitions: [
@@ -27,20 +28,18 @@ describe('addErrorsAndRunHooks', () => {
Icon: null,
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.NUMBER,
- } as SpreadsheetImportField;
+ };
- const uniqueField = {
+ const uniqueField: SpreadsheetImportField<'country'> = {
key: 'country',
label: 'Country',
fieldValidationDefinitions: [{ rule: 'unique' }],
Icon: null,
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.SELECT,
- fieldMetadataItemId: '2',
- isNestedField: false,
- } as SpreadsheetImportField;
+ };
- const functionValidationFieldTrue = {
+ const functionValidationFieldTrue: SpreadsheetImportField<'email'> = {
key: 'email',
label: 'Email',
fieldValidationDefinitions: [
@@ -53,11 +52,9 @@ describe('addErrorsAndRunHooks', () => {
Icon: null,
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.EMAILS,
- fieldMetadataItemId: '1',
- isNestedField: false,
- } as SpreadsheetImportField;
+ };
- const functionValidationFieldFalse = {
+ const functionValidationFieldFalse: SpreadsheetImportField<'email'> = {
key: 'email',
label: 'Email',
fieldValidationDefinitions: [
@@ -70,25 +67,23 @@ describe('addErrorsAndRunHooks', () => {
Icon: null,
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.EMAILS,
- fieldMetadataItemId: '3',
- isNestedField: false,
- } as SpreadsheetImportField;
+ };
- const validData: ImportedStructuredRow = {
+ const validData: ImportedStructuredRow<'name' | 'age'> = {
name: 'John',
age: '30',
};
- const dataWithoutNameAndInvalidAge: ImportedStructuredRow = {
+ const dataWithoutNameAndInvalidAge: ImportedStructuredRow<'name' | 'age'> = {
name: '',
age: 'Invalid',
};
- const dataWithDuplicatedValue: ImportedStructuredRow = {
+ const dataWithDuplicatedValue: FullData = {
name: 'Alice',
age: '40',
country: 'Brazil',
};
- const data: ImportedStructuredRow[] = [
+ const data: ImportedStructuredRow<'name' | 'age'>[] = [
validData,
dataWithoutNameAndInvalidAge,
];
@@ -118,14 +113,18 @@ describe('addErrorsAndRunHooks', () => {
level: 'error',
};
- 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;
- });
+ 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;
+ },
+ );
it('should correctly call rowHook and tableHook and add errors', () => {
const result = addErrorsAndRunHooks(
@@ -180,7 +179,7 @@ describe('addErrorsAndRunHooks', () => {
[
dataWithDuplicatedValue,
dataWithDuplicatedValue,
- ] as unknown as ImportedStructuredRow[],
+ ] as unknown as FullData[],
[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
new file mode 100644
index 000000000..84c1c0cc4
--- /dev/null
+++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findMatch.test.ts
@@ -0,0 +1,93 @@
+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 ed1f8e8c9..f6c875249 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 = {
+const nameField: SpreadsheetImportField<'Name'> = {
key: 'Name',
label: 'Name',
Icon: null,
@@ -15,11 +15,9 @@ const nameField: SpreadsheetImportField = {
type: 'input',
},
fieldMetadataType: FieldMetadataType.TEXT,
- fieldMetadataItemId: '1',
- isNestedField: false,
};
-const ageField: SpreadsheetImportField = {
+const ageField: SpreadsheetImportField<'Age'> = {
key: 'Age',
label: 'Age',
Icon: null,
@@ -27,37 +25,37 @@ const ageField: SpreadsheetImportField = {
type: 'input',
},
fieldMetadataType: FieldMetadataType.NUMBER,
- fieldMetadataItemId: '2',
- isNestedField: false,
};
const validations: SpreadsheetImportFieldValidationDefinition[] = [
{ rule: 'required' },
];
-const nameFieldWithValidations: SpreadsheetImportField = {
+const nameFieldWithValidations: SpreadsheetImportField<'Name'> = {
...nameField,
fieldValidationDefinitions: validations,
};
-const ageFieldWithValidations: SpreadsheetImportField = {
+const ageFieldWithValidations: SpreadsheetImportField<'Age'> = {
...ageField,
fieldValidationDefinitions: validations,
};
-const nameColumn: SpreadsheetColumn = {
+type ColumnValues = 'Name' | 'Age';
+
+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 cfdc07610..8b770c964 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[] = [
+ const fields: SpreadsheetImportField<'Options' | 'Name'>[] = [
{
key: 'Options',
Icon: null,
@@ -27,8 +27,6 @@ describe('getFieldOptions', () => {
options: optionsArray,
},
fieldMetadataType: FieldMetadataType.SELECT,
- fieldMetadataItemId: '1',
- isNestedField: false,
},
{
key: 'Name',
@@ -38,8 +36,6 @@ 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
new file mode 100644
index 000000000..82b8e063d
--- /dev/null
+++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getMatchedColumns.test.ts
@@ -0,0 +1,166 @@
+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 5d242a3b2..527f7ca1c 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 = [
+ const fields: SpreadsheetImportField[] = [
{
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 = [
+ const fields: SpreadsheetImportField[] = [
{
key: 'active',
label: 'Active',
@@ -89,10 +89,8 @@ describe('normalizeTableData', () => {
},
fieldMetadataType: FieldMetadataType.BOOLEAN,
Icon: null,
- fieldMetadataItemId: '1',
- isNestedField: false,
},
- ] as SpreadsheetImportField[];
+ ];
const rawData = [['Yes'], ['No'], ['OtherValue']];
@@ -102,7 +100,7 @@ describe('normalizeTableData', () => {
});
it('should map matchedSelect and matchedSelectOptions values correctly', () => {
- const columns: SpreadsheetColumn[] = [
+ const columns: SpreadsheetColumn[] = [
{
index: 0,
header: 'Number',
@@ -115,7 +113,7 @@ describe('normalizeTableData', () => {
},
];
- const fields = [
+ const fields: SpreadsheetImportField[] = [
{
key: 'number',
label: 'Number',
@@ -129,7 +127,7 @@ describe('normalizeTableData', () => {
fieldMetadataType: FieldMetadataType.SELECT,
Icon: null,
},
- ] as SpreadsheetImportField[];
+ ];
const rawData = [['One'], ['Two'], ['OtherValue']];
@@ -143,7 +141,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 },
];
@@ -156,7 +154,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 fa6a87c4e..0aae11bab 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 = {
+ const defaultField: SpreadsheetImportField<'Name'> = {
Icon: null,
label: 'label',
key: 'Name',
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.TEXT,
- } as SpreadsheetImportField;
+ };
- const oldColumn: SpreadsheetColumn = {
+ const oldColumn: SpreadsheetColumn<'oldValue'> = {
index: 0,
header: 'Name',
type: SpreadsheetColumnType.matched,
@@ -27,7 +27,7 @@ describe('setColumn', () => {
type: 'select',
options: [{ value: 'John' }, { value: 'Alice' }],
},
- } as SpreadsheetImportField;
+ } as SpreadsheetImportField<'Name'>;
const data = [['John'], ['Alice']];
const result = setColumn(oldColumn, field, data);
@@ -54,7 +54,7 @@ describe('setColumn', () => {
const field = {
...defaultField,
fieldType: { type: 'checkbox' },
- } as SpreadsheetImportField;
+ } as SpreadsheetImportField<'Name'>;
const result = setColumn(oldColumn, field);
@@ -70,7 +70,7 @@ describe('setColumn', () => {
const field = {
...defaultField,
fieldType: { type: 'input' },
- } as SpreadsheetImportField;
+ } as SpreadsheetImportField<'Name'>;
const result = setColumn(oldColumn, field);
@@ -86,7 +86,7 @@ describe('setColumn', () => {
const field = {
...defaultField,
fieldType: { type: 'unknown' },
- } as unknown as SpreadsheetImportField;
+ } as unknown as SpreadsheetImportField<'Name'>;
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 dddf0868c..edf3345f9 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 = {
+ const column: SpreadsheetColumn<'John'> = {
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 e08bf6713..e764f4b6e 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 = {
+ const oldColumn: SpreadsheetColumn<'John' | ''> = {
index: 0,
header: 'Name',
type: SpreadsheetColumnType.matchedSelect,
@@ -32,7 +32,7 @@ describe('setSubColumn', () => {
});
it('should return a matchedSelectOptionsColumn with updated matchedOptions', () => {
- const oldColumn: SpreadsheetColumn = {
+ const oldColumn: SpreadsheetColumn<'John' | 'Jane'> = {
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 ef509e39d..c06cebfde 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/utils/dataMutations.ts
+++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/dataMutations.ts
@@ -15,17 +15,17 @@ import {
import { isDefined } from 'twenty-shared/utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
-export const addErrorsAndRunHooks = (
- data: (ImportedStructuredRow & Partial)[],
- fields: SpreadsheetImportFields,
- rowHook?: SpreadsheetImportRowHook,
- tableHook?: SpreadsheetImportTableHook,
-): (ImportedStructuredRow & ImportedStructuredRowMetadata)[] => {
+export const addErrorsAndRunHooks = (
+ data: (ImportedStructuredRow & Partial)[],
+ fields: SpreadsheetImportFields,
+ rowHook?: SpreadsheetImportRowHook,
+ tableHook?: SpreadsheetImportTableHook,
+): (ImportedStructuredRow & ImportedStructuredRowMetadata)[] => {
const errors: Errors = {};
const addHookError = (
rowIndex: number,
- fieldKey: string,
+ fieldKey: T,
error: SpreadsheetImportInfo,
) => {
errors[rowIndex] = {
@@ -48,7 +48,7 @@ export const addErrorsAndRunHooks = (
field.fieldValidationDefinitions?.forEach((fieldValidationDefinition) => {
switch (fieldValidationDefinition.rule) {
case 'unique': {
- const values = data.map((entry) => entry[field.key]);
+ const values = data.map((entry) => entry[field.key as T]);
const taken = new Set(); // Set of items used at least once
const duplicates = new Set(); // Set of items used multiple times
@@ -87,9 +87,9 @@ export const addErrorsAndRunHooks = (
case 'required': {
data.forEach((entry, index) => {
if (
- entry[field.key] === null ||
- entry[field.key] === undefined ||
- entry[field.key] === ''
+ entry[field.key as T] === null ||
+ entry[field.key as T] === undefined ||
+ entry[field.key as T] === ''
) {
errors[index] = {
...errors[index],
@@ -156,17 +156,14 @@ export const addErrorsAndRunHooks = (
if (!('__index' in value)) {
value.__index = v4();
}
- const newValue = value as ImportedStructuredRow &
+ const newValue = value as ImportedStructuredRow &
ImportedStructuredRowMetadata;
if (isDefined(errors[index])) {
- return { ...newValue, __errors: errors[index] } as ImportedStructuredRow &
- ImportedStructuredRowMetadata;
+ return { ...newValue, __errors: errors[index] };
}
-
if (isUndefinedOrNull(errors[index]) && isDefined(value?.__errors)) {
- return { ...newValue, __errors: null } as ImportedStructuredRow &
- ImportedStructuredRowMetadata;
+ return { ...newValue, __errors: null };
}
return newValue;
});
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/findMatch.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/findMatch.ts
new file mode 100644
index 000000000..6a6659697
--- /dev/null
+++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/findMatch.ts
@@ -0,0 +1,30 @@
+import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
+import lavenstein from 'js-levenshtein';
+
+type AutoMatchAccumulator = {
+ distance: number;
+ value: T;
+};
+
+export const findMatch = (
+ header: string,
+ fields: SpreadsheetImportFields,
+ autoMapDistance: number,
+): T | undefined => {
+ const smallestValue = fields.reduce>((acc, field) => {
+ const distance = Math.min(
+ ...[
+ lavenstein(field.key, header),
+ ...(field.alternateMatches?.map((alternate) =>
+ lavenstein(alternate, header),
+ ) || []),
+ ],
+ );
+ return distance < acc.distance || acc.distance === undefined
+ ? ({ value: field.key, distance } as AutoMatchAccumulator)
+ : acc;
+ }, {} as AutoMatchAccumulator);
+ return smallestValue.distance <= autoMapDistance
+ ? smallestValue.value
+ : undefined;
+};
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts
index b029fb7ed..f31908931 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts
+++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts
@@ -1,9 +1,9 @@
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
-export const findUnmatchedRequiredFields = (
- fields: SpreadsheetImportFields,
- columns: SpreadsheetColumns,
+export const findUnmatchedRequiredFields = (
+ fields: SpreadsheetImportFields,
+ columns: SpreadsheetColumns,
) =>
fields
.filter((field) =>
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/getFieldOptions.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/getFieldOptions.ts
index 6dc0e214a..eb867c25f 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/utils/getFieldOptions.ts
+++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/getFieldOptions.ts
@@ -1,7 +1,7 @@
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
-export const getFieldOptions = (
- fields: SpreadsheetImportFields,
+export const getFieldOptions = (
+ fields: SpreadsheetImportFields,
fieldKey: string,
) => {
const field = fields.find(({ key }) => fieldKey === key);
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumns.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumns.ts
new file mode 100644
index 000000000..b52c0ae1e
--- /dev/null
+++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumns.ts
@@ -0,0 +1,52 @@
+import lavenstein from 'js-levenshtein';
+
+import { MatchColumnsStepProps } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
+
+import {
+ SpreadsheetImportField,
+ SpreadsheetImportFields,
+} from '@/spreadsheet-import/types';
+import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
+import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
+import { isDefined } from 'twenty-shared/utils';
+import { findMatch } from './findMatch';
+import { setColumn } from './setColumn';
+
+export const getMatchedColumns = (
+ columns: SpreadsheetColumns,
+ fields: SpreadsheetImportFields,
+ data: MatchColumnsStepProps['data'],
+ autoMapDistance: number,
+) =>
+ columns.reduce