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 }) => ( - handleSubFieldSelect(value)} - LeftIcon={Icon} - text={shortLabelForNestedField} - disabled={disabled} - /> - ), - )} + {subFieldsThatExistInOptions.map(({ subFieldName }) => ( + handleSubFieldSelect(subFieldName)} + LeftIcon={getIcon(fieldMetadataItem.icon)} + text={getCompositeSubFieldLabel( + fieldMetadataItem.type as CompositeFieldType, + subFieldName, + )} + disabled={ + options.find( + (option) => + option.value === + getSubFieldOptionKey(fieldMetadataItem, subFieldName), + )?.disabled + } + /> + ))} ); 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[]>((arr, column) => { + const autoMatch = findMatch(column.header, fields, autoMapDistance); + if (isDefined(autoMatch)) { + const field = fields.find( + (field) => field.key === autoMatch, + ) as SpreadsheetImportField; + const duplicateIndex = arr.findIndex( + (column) => 'value' in column && column.value === field.key, + ); + const duplicate = arr[duplicateIndex]; + if (duplicate && 'value' in duplicate) { + return lavenstein(duplicate.value, duplicate.header) < + lavenstein(autoMatch, column.header) + ? [ + ...arr.slice(0, duplicateIndex), + setColumn(arr[duplicateIndex], field, data), + ...arr.slice(duplicateIndex + 1), + setColumn(column), + ] + : [ + ...arr.slice(0, duplicateIndex), + setColumn(arr[duplicateIndex]), + ...arr.slice(duplicateIndex + 1), + setColumn(column, field, data), + ]; + } else { + return [...arr, setColumn(column, field, data)]; + } + } else { + return [...arr, column]; + } + }, []); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumnsWithFuse.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumnsWithFuse.ts index 0383ff76e..0a3bab326 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumnsWithFuse.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumnsWithFuse.ts @@ -11,16 +11,16 @@ import { setColumn } from '@/spreadsheet-import/utils/setColumn'; import Fuse from 'fuse.js'; import { isDefined } from 'twenty-shared/utils'; -export const getMatchedColumnsWithFuse = ({ +export const getMatchedColumnsWithFuse = ({ columns, fields, data, }: { - columns: SpreadsheetColumns; - fields: SpreadsheetImportFields; + columns: SpreadsheetColumns; + fields: SpreadsheetImportFields; data: MatchColumnsStepProps['data']; }) => { - const matchedColumns: SpreadsheetColumn[] = []; + const matchedColumns: SpreadsheetColumn[] = []; const fieldsToSearch = new Fuse(fields, { keys: ['label'], @@ -30,8 +30,8 @@ export const getMatchedColumnsWithFuse = ({ }); const suggestedFieldsByColumnHeader: Record< - SpreadsheetColumn['header'], - SpreadsheetImportField[] + SpreadsheetColumn['header'], + SpreadsheetImportField[] > = {}; for (const column of columns) { @@ -58,7 +58,7 @@ export const getMatchedColumnsWithFuse = ({ ); suggestedFieldsByColumnHeader[column.header] = fieldsThatMatch.map( - (match) => match.item as SpreadsheetImportField, + (match) => match.item as SpreadsheetImportField, ); if (isFirstMatchValid && isFieldStillUnmatched) { diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/getShortNestedFieldLabel.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/getShortNestedFieldLabel.ts deleted file mode 100644 index 7305ba763..000000000 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/getShortNestedFieldLabel.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const getShortNestedFieldLabel = (label: string) => { - return label.split(' / ').slice(1).join(' / '); -}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/normalizeTableData.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/normalizeTableData.ts index d0fcc89d6..368fe71f3 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/normalizeTableData.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/normalizeTableData.ts @@ -9,10 +9,10 @@ import { isDefined } from 'twenty-shared/utils'; import { z } from 'zod'; import { normalizeCheckboxValue } from './normalizeCheckboxValue'; -export const normalizeTableData = ( - columns: SpreadsheetColumns, +export const normalizeTableData = ( + columns: SpreadsheetColumns, data: ImportedRow[], - fields: SpreadsheetImportFields, + fields: SpreadsheetImportFields, ) => data.map((row) => columns.reduce((acc, column, index) => { @@ -101,5 +101,5 @@ export const normalizeTableData = ( default: return acc; } - }, {} as ImportedStructuredRow), + }, {} as ImportedStructuredRow), ); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/setColumn.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/setColumn.ts index 98bcb2b79..c2cfd802c 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/setColumn.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/setColumn.ts @@ -9,17 +9,17 @@ import { isDefined } from 'twenty-shared/utils'; import { z } from 'zod'; import { uniqueEntries } from './uniqueEntries'; -export const setColumn = ( - oldColumn: SpreadsheetColumn, - field?: SpreadsheetImportField, +export const setColumn = ( + oldColumn: SpreadsheetColumn, + field?: SpreadsheetImportField, data?: MatchColumnsStepProps['data'], -): SpreadsheetColumn => { +): SpreadsheetColumn => { if (field?.fieldType.type === 'select') { const fieldOptions = field.fieldType.options; const uniqueData = uniqueEntries( data || [], oldColumn.index, - ) as SpreadsheetMatchedOptions[]; + ) as SpreadsheetMatchedOptions[]; const matchedOptions = uniqueData.map((record) => { const value = fieldOptions.find( @@ -28,8 +28,8 @@ export const setColumn = ( fieldOption.label === record.entry, )?.value; return value - ? ({ ...record, value } as SpreadsheetMatchedOptions) - : (record as SpreadsheetMatchedOptions); + ? ({ ...record, value } as SpreadsheetMatchedOptions) + : (record as SpreadsheetMatchedOptions); }); const allMatched = matchedOptions.filter((o) => o.value).length === uniqueData?.length; @@ -77,8 +77,8 @@ export const setColumn = ( fieldOption.value === entry || fieldOption.label === entry, )?.value; return value - ? ({ entry, value } as SpreadsheetMatchedOptions) - : ({ entry } as SpreadsheetMatchedOptions); + ? ({ entry, value } as SpreadsheetMatchedOptions) + : ({ entry } as SpreadsheetMatchedOptions); }); const areAllMatched = matchedOptions.filter((option) => option.value).length === diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/setIgnoreColumn.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/setIgnoreColumn.ts index 9d30c7684..7123369cd 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/setIgnoreColumn.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/setIgnoreColumn.ts @@ -1,10 +1,10 @@ import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn'; import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType'; -export const setIgnoreColumn = ({ +export const setIgnoreColumn = ({ header, index, -}: SpreadsheetColumn): SpreadsheetColumn => ({ +}: SpreadsheetColumn): SpreadsheetColumn => ({ header, index, type: SpreadsheetColumnType.ignored, diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/setSubColumn.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/setSubColumn.ts index a4ca89461..5bd63010d 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/setSubColumn.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/setSubColumn.ts @@ -5,13 +5,15 @@ import { import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType'; import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions'; -export const setSubColumn = ( +export const setSubColumn = ( oldColumn: - | SpreadsheetMatchedSelectColumn - | SpreadsheetMatchedSelectOptionsColumn, + | SpreadsheetMatchedSelectColumn + | SpreadsheetMatchedSelectOptionsColumn, entry: string, value: string, -): SpreadsheetMatchedSelectColumn | SpreadsheetMatchedSelectOptionsColumn => { +): + | SpreadsheetMatchedSelectColumn + | SpreadsheetMatchedSelectOptionsColumn => { const shouldUnselectValue = oldColumn.matchedOptions.find((option) => option.entry === entry)?.value === value; @@ -26,13 +28,13 @@ export const setSubColumn = ( if (allMatched) { return { ...oldColumn, - matchedOptions: options as SpreadsheetMatchedOptions[], + matchedOptions: options as SpreadsheetMatchedOptions[], type: SpreadsheetColumnType.matchedSelectOptions, }; } else { return { ...oldColumn, - matchedOptions: options as SpreadsheetMatchedOptions[], + matchedOptions: options as SpreadsheetMatchedOptions[], type: SpreadsheetColumnType.matchedSelect, }; } diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetBuildFieldOptions.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetBuildFieldOptions.ts new file mode 100644 index 000000000..359eb7910 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetBuildFieldOptions.ts @@ -0,0 +1,29 @@ +import { getFieldMetadataTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFieldMetadataTypeLabel'; +import { SpreadsheetImportFields } from '@/spreadsheet-import/types'; +import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns'; +import { FieldMetadataType } from 'twenty-shared/types'; + +export const spreadsheetBuildFieldOptions = ( + fields: SpreadsheetImportFields, + columns: SpreadsheetColumns, +) => { + return fields + .filter((field) => field.fieldMetadataType !== FieldMetadataType.RICH_TEXT) + .map(({ Icon, label, key, fieldMetadataType }) => { + const isSelected = + columns.findIndex((column) => { + if ('value' in column) { + return column.value === key; + } + return false; + }) !== -1; + + return { + Icon: Icon, + value: key, + label: label, + disabled: isSelected, + fieldMetadataTypeLabel: getFieldMetadataTypeLabel(fieldMetadataType), + } as const; + }); +}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportBuildFieldOptions.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportBuildFieldOptions.ts deleted file mode 100644 index b3f649f3a..000000000 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportBuildFieldOptions.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { getFieldMetadataTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFieldMetadataTypeLabel'; -import { SpreadsheetImportFields } from '@/spreadsheet-import/types'; -import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns'; -import { SpreadsheetImportFieldOption } from '@/spreadsheet-import/types/SpreadsheetImportFieldOption'; -import { getShortNestedFieldLabel } from '@/spreadsheet-import/utils/getShortNestedFieldLabel'; -import { ReadonlyDeep } from 'type-fest'; - -export const spreadsheetImportBuildFieldOptions = ( - fields: SpreadsheetImportFields, - columns: SpreadsheetColumns, -): readonly ReadonlyDeep[] => { - return fields.map( - ({ - Icon, - label, - key, - fieldMetadataType, - isNestedField, - fieldMetadataItemId, - }) => { - const isSelected = columns.some((column) => { - if ('value' in column) { - return column.value === key; - } - return false; - }); - - return { - Icon: Icon, - value: key, - label, - shortLabelForNestedField: isNestedField - ? getShortNestedFieldLabel(label) - : undefined, - disabled: isSelected, - fieldMetadataTypeLabel: getFieldMetadataTypeLabel(fieldMetadataType), - isNestedField: isNestedField, - fieldMetadataItemId: fieldMetadataItemId, - }; - }, - ); -}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportGetSubFieldOptions.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportGetSubFieldOptions.ts deleted file mode 100644 index 00f483a5e..000000000 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportGetSubFieldOptions.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { SpreadsheetImportFieldOption } from '@/spreadsheet-import/types/SpreadsheetImportFieldOption'; - -export const getSubFieldOptions = ( - fieldMetadataItem: FieldMetadataItem, - options: readonly Readonly[], - searchFilter: string, -): readonly Readonly[] => { - return options.filter( - (option) => - option.fieldMetadataItemId === fieldMetadataItem.id && - option.label.toLowerCase().includes(searchFilter.toLowerCase()), - ); -}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportHasNestedFields.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportHasNestedFields.ts deleted file mode 100644 index 20025455e..000000000 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetImportHasNestedFields.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; -import { FieldMetadataType } from 'twenty-shared/types'; -import { RelationType } from '~/generated/graphql'; - -export const hasNestedFields = (fieldMetadata: FieldMetadataItem) => { - return ( - (fieldMetadata.type === FieldMetadataType.RELATION && - fieldMetadata.relation?.type === RelationType.MANY_TO_ONE) || - isCompositeFieldType(fieldMetadata.type) - ); -}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/uniqueEntries.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/uniqueEntries.ts index 32e079ba9..16d825de4 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/uniqueEntries.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/uniqueEntries.ts @@ -3,10 +3,10 @@ import uniqBy from 'lodash.uniqby'; import { MatchColumnsStepProps } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions'; -export const uniqueEntries = ( +export const uniqueEntries = ( data: MatchColumnsStepProps['data'], index: number, -): Partial[] => +): Partial>[] => uniqBy( data.map((row) => ({ entry: row[index] })), 'entry', diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-connect-input-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-connect-input-type-definition.factory.ts index 8a57494af..ef7d8ad97 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-connect-input-type-definition.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-connect-input-type-definition.factory.ts @@ -6,7 +6,6 @@ import { GraphQLInputType, GraphQLString, } from 'graphql'; -import { getUniqueConstraintsFields } from 'twenty-shared/utils'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; @@ -18,6 +17,7 @@ import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-build import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { getUniqueConstraintsFields } from 'src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util'; import { pascalCase } from 'src/utils/pascal-case'; export const formatRelationConnectInputTarget = (objectMetadataId: string) => diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type.ts index cc392f938..144abcec1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type.ts @@ -10,14 +10,14 @@ export const fullNameCompositeType: CompositeType = { type: FieldMetadataType.TEXT, hidden: false, isRequired: false, - isIncludedInUniqueConstraint: false, + isIncludedInUniqueConstraint: true, }, { name: 'lastName', type: FieldMetadataType.TEXT, hidden: false, isRequired: false, - isIncludedInUniqueConstraint: false, + isIncludedInUniqueConstraint: true, }, ], }; diff --git a/packages/twenty-shared/src/utils/indexMetadata/__tests__/getUniqueConstraintsFields.test.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/__tests__/getUniqueConstraintsFields.util.spec.ts similarity index 94% rename from packages/twenty-shared/src/utils/indexMetadata/__tests__/getUniqueConstraintsFields.test.ts rename to packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/__tests__/getUniqueConstraintsFields.util.spec.ts index cb04407e2..1875ab8f9 100644 --- a/packages/twenty-shared/src/utils/indexMetadata/__tests__/getUniqueConstraintsFields.test.ts +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/__tests__/getUniqueConstraintsFields.util.spec.ts @@ -1,5 +1,6 @@ -import { FieldMetadataType } from '@/types'; -import { getUniqueConstraintsFields } from '../getUniqueConstraintsFields'; +import { FieldMetadataType } from 'twenty-shared/types'; + +import { getUniqueConstraintsFields } from 'src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util'; describe('getUniqueConstraintsFields', () => { const mockIdField = { diff --git a/packages/twenty-shared/src/utils/indexMetadata/getUniqueConstraintsFields.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util.ts similarity index 95% rename from packages/twenty-shared/src/utils/indexMetadata/getUniqueConstraintsFields.ts rename to packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util.ts index b72e0ddee..be70d2b47 100644 --- a/packages/twenty-shared/src/utils/indexMetadata/getUniqueConstraintsFields.ts +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util.ts @@ -1,4 +1,4 @@ -import { isDefined } from '@/utils/validation/isDefined'; +import { isDefined } from 'twenty-shared/utils'; export const getUniqueConstraintsFields = < K extends { diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts index f16dddc7e..bef818409 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts @@ -1560,9 +1560,7 @@ export class WorkspaceEntityManager extends EntityManager { const connectFieldName = connectQueryConfig.connectFieldName; throw new TwentyORMException( - `Expected 1 record to connect to ${connectFieldName}, but found ${recordToConnectTotal} with conditions: ${JSON.stringify( - connectQueryConfig.recordToConnectConditionByEntityIndex[index], - )}.`, + `Expected 1 record to connect to ${connectFieldName}, but found ${recordToConnectTotal}.`, TwentyORMExceptionCode.CONNECT_RECORD_NOT_FOUND, ); } diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts index b93fa0d09..2ad109776 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts @@ -1,13 +1,14 @@ import { t } from '@lingui/core/macro'; import deepEqual from 'deep-equal'; import { FieldMetadataType } from 'twenty-shared/types'; -import { getUniqueConstraintsFields, isDefined } from 'twenty-shared/utils'; +import { isDefined } from 'twenty-shared/utils'; -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; +import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { getUniqueConstraintsFields } from 'src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { ConnectObject } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-relation-connect.type'; @@ -218,14 +219,14 @@ const hasRelationConnect = (value: unknown): value is ConnectObject => { return whereKeys.every((key) => { const whereValue = where[key]; - if (typeof whereValue === 'string' || whereValue === null) { + if (typeof whereValue === 'string') { return true; } if (whereValue && typeof whereValue === 'object') { const subObj = whereValue as Record; return Object.values(subObj).every( - (subValue) => typeof subValue === 'string' || subValue === null, + (subValue) => typeof subValue === 'string', ); } diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/relation-connect.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/relation-connect.integration-spec.ts index 41f39243f..7aee5a75e 100644 --- a/packages/twenty-server/test/integration/graphql/suites/object-generated/relation-connect.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/relation-connect.integration-spec.ts @@ -29,11 +29,11 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)', data: [ { id: TEST_COMPANY_1_ID, - domainName: { primaryLinkUrl: 'https://company1.com' }, + domainName: { primaryLinkUrl: 'company1.com' }, }, { id: TEST_COMPANY_2_ID, - domainName: { primaryLinkUrl: 'https://company2.com' }, + domainName: { primaryLinkUrl: 'company2.com' }, }, ], }); @@ -58,7 +58,7 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)', id: TEST_PERSON_1_ID, company: { connect: { - where: { domainName: { primaryLinkUrl: 'https://company1.com' } }, + where: { domainName: { primaryLinkUrl: 'company1.com' } }, }, }, }, @@ -81,7 +81,7 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)', id: TEST_PERSON_1_ID, company: { connect: { - where: { domainName: { primaryLinkUrl: 'https://company1.com' } }, + where: { domainName: { primaryLinkUrl: 'company1.com' } }, }, }, }, @@ -89,7 +89,7 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)', id: TEST_PERSON_2_ID, company: { connect: { - where: { domainName: { primaryLinkUrl: 'https://company2.com' } }, + where: { domainName: { primaryLinkUrl: 'company2.com' } }, }, }, }, @@ -152,7 +152,7 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)', expect(response.body.errors).toBeDefined(); expect(response.body.errors[0].message).toBe( - 'Expected 1 record to connect to company, but found 0 with conditions: [["domainNamePrimaryLinkUrl","not-existing-company"]].', + 'Expected 1 record to connect to company, but found 0.', ); expect(response.body.errors[0].extensions.code).toBe( ErrorCode.BAD_USER_INPUT, diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts index 2aa55772f..acfb641fb 100644 --- a/packages/twenty-shared/src/utils/index.ts +++ b/packages/twenty-shared/src/utils/index.ts @@ -15,7 +15,6 @@ export { sanitizeURL, getLogoUrlFromDomainName, } from './image/getLogoUrlFromDomainName'; -export { getUniqueConstraintsFields } from './indexMetadata/getUniqueConstraintsFields'; export { parseJson } from './parseJson'; export { removeUndefinedFields } from './removeUndefinedFields'; export { getGenericOperationName } from './sentry/getGenericOperationName'; diff --git a/packages/twenty-shared/src/utils/indexMetadata/index.ts b/packages/twenty-shared/src/utils/indexMetadata/index.ts deleted file mode 100644 index 96de704c1..000000000 --- a/packages/twenty-shared/src/utils/indexMetadata/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './getUniqueConstraintsFields';