diff --git a/packages/twenty-front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts b/packages/twenty-front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts deleted file mode 100644 index 9b26fb0b55..0000000000 --- a/packages/twenty-front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Company } from '@/companies/types/Company'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords'; -import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport'; -import { SpreadsheetOptions } from '@/spreadsheet-import/types'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; - -import { fieldsForCompany } from '../utils/fieldsForCompany'; - -export type FieldCompanyMapping = (typeof fieldsForCompany)[number]['key']; - -export const useSpreadsheetCompanyImport = () => { - const { openSpreadsheetImport } = useSpreadsheetImport(); - const { enqueueSnackBar } = useSnackBar(); - - const { createManyRecords: createManyCompanies } = - useCreateManyRecords({ - objectNameSingular: CoreObjectNameSingular.Company, - }); - - const openCompanySpreadsheetImport = ( - options?: Omit< - SpreadsheetOptions, - 'fields' | 'isOpen' | 'onClose' - >, - ) => { - openSpreadsheetImport({ - ...options, - onSubmit: async (data) => { - // TODO: Add better type checking in spreadsheet import later - const createInputs = data.validData.map( - (company) => - ({ - name: company.name as string | undefined, - domainName: company.domainName as string | undefined, - ...(company.linkedinUrl - ? { - linkedinLink: { - label: 'linkedinUrl', - url: company.linkedinUrl as string | undefined, - }, - } - : {}), - ...(company.annualRecurringRevenue - ? { - annualRecurringRevenue: { - amountMicros: Number(company.annualRecurringRevenue), - currencyCode: 'USD', - }, - } - : {}), - idealCustomerProfile: - company.idealCustomerProfile && - ['true', true].includes(company.idealCustomerProfile), - ...(company.xUrl - ? { - xLink: { - label: 'xUrl', - url: company.xUrl as string | undefined, - }, - } - : {}), - address: company.address as string | undefined, - employees: company.employees - ? Number(company.employees) - : undefined, - }) as Company, - ); - // TODO: abstract this part for any object - try { - await createManyCompanies(createInputs); - } catch (error: any) { - enqueueSnackBar(error?.message || 'Something went wrong', { - variant: 'error', - }); - } - }, - fields: fieldsForCompany, - }); - }; - - return { openCompanySpreadsheetImport }; -}; diff --git a/packages/twenty-front/src/modules/companies/utils/fieldsForCompany.tsx b/packages/twenty-front/src/modules/companies/utils/fieldsForCompany.tsx deleted file mode 100644 index 28388a6ae9..0000000000 --- a/packages/twenty-front/src/modules/companies/utils/fieldsForCompany.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { - IconBrandLinkedin, - IconBrandX, - IconBuildingSkyscraper, - IconMail, - IconMap, - IconMoneybag, - IconTarget, - IconUsers, -} from '@/ui/display/icon'; - -export const fieldsForCompany = [ - { - icon: IconBuildingSkyscraper, - label: 'Name', - key: 'name', - alternateMatches: ['name', 'company name', 'company'], - fieldType: { - type: 'input', - }, - example: 'Tim', - }, - { - icon: IconMail, - label: 'Domain name', - key: 'domainName', - alternateMatches: ['domain', 'domain name'], - fieldType: { - type: 'input', - }, - example: 'apple.dev', - }, - { - icon: IconBrandLinkedin, - label: 'Linkedin URL', - key: 'linkedinUrl', - alternateMatches: ['linkedIn', 'linkedin', 'linkedin url'], - fieldType: { - type: 'input', - }, - example: 'https://www.linkedin.com/in/apple', - }, - { - icon: IconMoneybag, - label: 'ARR', - key: 'annualRecurringRevenue', - alternateMatches: [ - 'arr', - 'annual revenue', - 'revenue', - 'recurring revenue', - 'annual recurring revenue', - ], - fieldType: { - type: 'input', - }, - validation: [ - { - regex: /^(\d+)?$/, - errorMessage: 'Annual recurring revenue must be a number', - level: 'error', - }, - ], - example: '1000000', - }, - { - icon: IconTarget, - label: 'ICP', - key: 'idealCustomerProfile', - alternateMatches: [ - 'icp', - 'ideal profile', - 'ideal customer profile', - 'ideal customer', - ], - fieldType: { - type: 'input', - }, - validation: [ - { - regex: /^(true|false)?$/, - errorMessage: 'Ideal custoner profile must be a boolean', - level: 'error', - }, - ], - example: 'true/false', - }, - { - icon: IconBrandX, - label: 'x URL', - key: 'xUrl', - alternateMatches: ['x', 'twitter', 'twitter url', 'x url'], - fieldType: { - type: 'input', - }, - example: 'https://x.com/tim_cook', - }, - { - icon: IconMap, - label: 'Address', - key: 'address', - fieldType: { - type: 'input', - }, - example: 'Maple street', - }, - { - icon: IconUsers, - label: 'Employees', - key: 'employees', - alternateMatches: ['employees', 'total employees', 'number of employees'], - fieldType: { - type: 'input', - }, - validation: [ - { - regex: /^\d+$/, - errorMessage: 'Employees must be a number', - level: 'error', - }, - ], - example: '150', - }, -] as const; diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx index 8c1f855970..ff79b34e30 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx @@ -5,8 +5,8 @@ import { Key } from 'ts-key-enum'; import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId'; import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard'; import { useRecordIndexOptionsForTable } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForTable'; -import { useRecordIndexOptionsImport } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsImport'; import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; +import { useSpreadsheetRecordImport } from '@/object-record/spreadsheet-import/useSpreadsheetRecordImport'; import { IconBaselineDensitySmall, IconChevronLeft, @@ -116,7 +116,8 @@ export const RecordIndexOptionsDropdownContent = ({ ? handleBoardFieldVisibilityChange : handleColumnVisibilityChange; - const { handleImport } = useRecordIndexOptionsImport({ objectNameSingular }); + const { openRecordSpreadsheetImport } = + useSpreadsheetRecordImport(objectNameSingular); return ( <> @@ -141,13 +142,11 @@ export const RecordIndexOptionsDropdownContent = ({ LeftIcon={IconTag} text="Fields" /> - {handleImport && ( - handleImport()} - LeftIcon={IconFileImport} - text="Import" - /> - )} + openRecordSpreadsheetImport()} + LeftIcon={IconFileImport} + text="Import" + /> )} diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordIndexOptionsImport.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordIndexOptionsImport.ts deleted file mode 100644 index 6acb8b3766..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordIndexOptionsImport.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useSpreadsheetCompanyImport } from '@/companies/hooks/useSpreadsheetCompanyImport'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useSpreadsheetPersonImport } from '@/people/hooks/useSpreadsheetPersonImport'; - -type useRecordIndexOptionsImportParams = { - objectNameSingular: string; -}; - -export const useRecordIndexOptionsImport = ({ - objectNameSingular, -}: useRecordIndexOptionsImportParams) => { - const { openPersonSpreadsheetImport } = useSpreadsheetPersonImport(); - const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport(); - - const handleImport = - CoreObjectNameSingular.Company === objectNameSingular - ? openCompanySpreadsheetImport - : CoreObjectNameSingular.Person === objectNameSingular - ? openPersonSpreadsheetImport - : undefined; - - return { handleImport }; -}; diff --git a/packages/twenty-front/src/modules/companies/hooks/__tests__/useSpreadsheetCompanyImport.test.tsx b/packages/twenty-front/src/modules/object-record/spreadsheet-import/__tests__/useSpreadsheetRecordImport.test.tsx similarity index 83% rename from packages/twenty-front/src/modules/companies/hooks/__tests__/useSpreadsheetCompanyImport.test.tsx rename to packages/twenty-front/src/modules/object-record/spreadsheet-import/__tests__/useSpreadsheetRecordImport.test.tsx index 57ac3d8c35..a26df2b1c8 100644 --- a/packages/twenty-front/src/modules/companies/hooks/__tests__/useSpreadsheetCompanyImport.test.tsx +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/__tests__/useSpreadsheetRecordImport.test.tsx @@ -4,10 +4,11 @@ import { MockedProvider } from '@apollo/client/testing'; import { act, renderHook, waitFor } from '@testing-library/react'; import { RecoilRoot, useRecoilValue } from 'recoil'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { spreadsheetImportState } from '@/spreadsheet-import/states/spreadsheetImportState'; import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; -import { useSpreadsheetCompanyImport } from '../useSpreadsheetCompanyImport'; +import { useSpreadsheetRecordImport } from '../useSpreadsheetRecordImport'; const companyId = 'cb2e9f4b-20c3-4759-9315-4ffeecfaf71a'; @@ -32,11 +33,11 @@ const companyMocks = [ variables: { data: [ { - name: 'Example Company', + address: 'test', domainName: 'example.com', + employees: 0, idealCustomerProfile: true, - address: undefined, - employees: undefined, + name: 'Example Company', id: companyId, }, ], @@ -75,21 +76,23 @@ describe('useSpreadsheetCompanyImport', () => { const { result } = renderHook( () => { const spreadsheetImport = useRecoilValue(spreadsheetImportState); - const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport(); - return { openCompanySpreadsheetImport, spreadsheetImport }; + const { openRecordSpreadsheetImport } = useSpreadsheetRecordImport( + CoreObjectNameSingular.Company, + ); + return { openRecordSpreadsheetImport, spreadsheetImport }; }, { wrapper: Wrapper, }, ); - const { spreadsheetImport, openCompanySpreadsheetImport } = result.current; + const { spreadsheetImport, openRecordSpreadsheetImport } = result.current; expect(spreadsheetImport.isOpen).toBe(false); expect(spreadsheetImport.options).toBeNull(); await act(async () => { - openCompanySpreadsheetImport(); + openRecordSpreadsheetImport(); }); const { spreadsheetImport: updatedImport } = result.current; @@ -109,8 +112,8 @@ describe('useSpreadsheetCompanyImport', () => { name: 'Example Company', domainName: 'example.com', idealCustomerProfile: true, - address: undefined, - employees: undefined, + address: 'test', + employees: '0', }, ], invalidData: [], @@ -121,8 +124,8 @@ describe('useSpreadsheetCompanyImport', () => { domainName: 'example.com', __index: 'cbc3985f-dde9-46d1-bae2-c124141700ac', idealCustomerProfile: true, - address: undefined, - employees: undefined, + address: 'test', + employees: '0', }, ], }, diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/useSpreadsheetRecordImport.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/useSpreadsheetRecordImport.ts new file mode 100644 index 0000000000..e2cc05e27c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/useSpreadsheetRecordImport.ts @@ -0,0 +1,166 @@ +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords'; +import { getSpreadSheetValidation } from '@/object-record/spreadsheet-import/util/getSpreadSheetValidation'; +import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport'; +import { SpreadsheetOptions, Validation } from '@/spreadsheet-import/types'; +import { useIcons } from '@/ui/display/icon/hooks/useIcons'; +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +const firstName = 'Firstname'; +const lastName = 'Lastname'; + +export const useSpreadsheetRecordImport = (objectNameSingular: string) => { + const { openSpreadsheetImport } = useSpreadsheetImport(); + const { enqueueSnackBar } = useSnackBar(); + const { getIcon } = useIcons(); + + const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular }); + const fields = objectMetadataItem.fields + .filter( + (x) => + x.isActive && + !x.isSystem && + x.name !== 'createdAt' && + (x.type !== FieldMetadataType.Relation || x.toRelationMetadata), + ) + .sort((a, b) => a.name.localeCompare(b.name)); + + const templateFields: { + icon: IconComponent; + label: string; + key: string; + fieldType: { + type: 'input' | 'checkbox'; + }; + validations?: Validation[]; + }[] = []; + for (const field of fields) { + if (field.type === FieldMetadataType.FullName) { + templateFields.push({ + icon: getIcon(field.icon), + label: `${firstName} (${field.label})`, + key: `${firstName} (${field.name})`, + fieldType: { + type: 'input', + }, + validations: getSpreadSheetValidation( + field.type, + `${firstName} (${field.label})`, + ), + }); + templateFields.push({ + icon: getIcon(field.icon), + label: `${lastName} (${field.label})`, + key: `${lastName} (${field.name})`, + fieldType: { + type: 'input', + }, + validations: getSpreadSheetValidation( + field.type, + `${lastName} (${field.label})`, + ), + }); + } else if (field.type === FieldMetadataType.Relation) { + templateFields.push({ + icon: getIcon(field.icon), + label: field.label + ' (ID)', + key: field.name, + fieldType: { + type: 'input', + }, + validations: getSpreadSheetValidation( + field.type, + field.label + ' (ID)', + ), + }); + } else { + templateFields.push({ + icon: getIcon(field.icon), + label: field.label, + key: field.name, + fieldType: { + type: 'input', + }, + validations: getSpreadSheetValidation(field.type, field.label), + }); + } + } + + const { createManyRecords } = useCreateManyRecords({ + objectNameSingular, + }); + + const openRecordSpreadsheetImport = ( + options?: Omit, 'fields' | 'isOpen' | 'onClose'>, + ) => { + openSpreadsheetImport({ + ...options, + onSubmit: async (data) => { + const createInputs = data.validData.map((record) => { + const fieldMapping: Record = {}; + for (const field of fields) { + const value = record[field.name]; + + switch (field.type) { + case FieldMetadataType.Boolean: + fieldMapping[field.name] = value === 'true' || value === true; + break; + case FieldMetadataType.Number: + case FieldMetadataType.Numeric: + fieldMapping[field.name] = Number(value); + break; + case FieldMetadataType.Currency: + if (value !== undefined) { + fieldMapping[field.name] = { + amountMicros: Number(value), + currencyCode: 'USD', + }; + } + break; + case FieldMetadataType.Link: + if (value !== undefined) { + fieldMapping[field.name] = { + label: field.name, + url: value || null, + }; + } + break; + case FieldMetadataType.Relation: + if (value) { + fieldMapping[field.name + 'Id'] = value; + } + break; + case FieldMetadataType.FullName: + if ( + record[`${firstName} (${field.name})`] || + record[`${lastName} (${field.name})`] + ) { + fieldMapping[field.name] = { + firstName: record[`${firstName} (${field.name})`] || '', + lastName: record[`${lastName} (${field.name})`] || '', + }; + } + break; + default: + fieldMapping[field.name] = value; + break; + } + } + return fieldMapping; + }); + try { + await createManyRecords(createInputs); + } catch (error: any) { + enqueueSnackBar(error?.message || 'Something went wrong', { + variant: 'error', + }); + } + }, + fields: templateFields, + }); + }; + + return { openRecordSpreadsheetImport }; +}; diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/util/getSpreadSheetValidation.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/util/getSpreadSheetValidation.ts new file mode 100644 index 0000000000..c546a3d19e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/util/getSpreadSheetValidation.ts @@ -0,0 +1,42 @@ +import { isValidPhoneNumber } from 'libphonenumber-js'; + +import { isValidUuid } from '@/object-record/spreadsheet-import/util/isValidUuid'; +import { Validation } from '@/spreadsheet-import/types'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +export const getSpreadSheetValidation = ( + type: FieldMetadataType, + fieldName: string, +): Validation[] => { + switch (type) { + case FieldMetadataType.Number: + return [ + { + rule: 'regex', + value: '^d+$', + errorMessage: fieldName + ' must be a number', + level: 'error', + }, + ]; + case FieldMetadataType.Phone: + return [ + { + rule: 'function', + isValid: (value: string) => isValidPhoneNumber(value), + errorMessage: fieldName + ' is not valid', + level: 'error', + }, + ]; + case FieldMetadataType.Relation: + return [ + { + rule: 'function', + isValid: (value: string) => isValidUuid(value), + errorMessage: fieldName + ' is not valid', + level: 'error', + }, + ]; + default: + return []; + } +}; diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/util/isValidUuid.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/util/isValidUuid.ts new file mode 100644 index 0000000000..322c982bd5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/util/isValidUuid.ts @@ -0,0 +1,5 @@ +export const isValidUuid = (value: string) => { + return /^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test( + value, + ); +}; diff --git a/packages/twenty-front/src/modules/people/hooks/__mocks__/useSpreadsheetPersonImport.ts b/packages/twenty-front/src/modules/people/hooks/__mocks__/useSpreadsheetPersonImport.ts deleted file mode 100644 index cd472f8c84..0000000000 --- a/packages/twenty-front/src/modules/people/hooks/__mocks__/useSpreadsheetPersonImport.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { gql } from '@apollo/client'; - -export const query = gql` - mutation CreatePeople($data: [PersonCreateInput!]!) { - createPeople(data: $data) { - id - opportunities { - edges { - node { - id - } - } - } - xLink { - label - url - } - id - pointOfContactForOpportunities { - edges { - node { - id - } - } - } - createdAt - company { - id - } - city - email - activityTargets { - edges { - node { - id - } - } - } - jobTitle - favorites { - edges { - node { - id - } - } - } - attachments { - edges { - node { - id - } - } - } - name { - firstName - lastName - } - phone - linkedinLink { - label - url - } - updatedAt - avatarUrl - companyId - } - } -`; - -export const personId = 'cb2e9f4b-20c3-4759-9315-4ffeecfaf71a'; - -export const variables = { - data: [ - { - id: personId, - name: { firstName: 'Sheldon', lastName: ' Cooper' }, - email: undefined, - jobTitle: undefined, - phone: undefined, - city: undefined, - }, - ], -}; - -export const responseData = [ - { - opportunities: { - edges: [], - }, - xLink: { - label: '', - url: '', - }, - pointOfContactForOpportunities: { - edges: [], - }, - createdAt: '', - company: { - id: '', - }, - city: '', - email: '', - activityTargets: { - edges: [], - }, - jobTitle: '', - favorites: { - edges: [], - }, - attachments: { - edges: [], - }, - name: variables.data[0].name, - phone: '', - linkedinLink: { - label: '', - url: '', - }, - updatedAt: '', - avatarUrl: '', - companyId: '', - id: personId, - }, -]; diff --git a/packages/twenty-front/src/modules/people/hooks/__tests__/useSpreadsheetPersonImport.test.tsx b/packages/twenty-front/src/modules/people/hooks/__tests__/useSpreadsheetPersonImport.test.tsx deleted file mode 100644 index f3f1a46807..0000000000 --- a/packages/twenty-front/src/modules/people/hooks/__tests__/useSpreadsheetPersonImport.test.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; -import { act, renderHook, waitFor } from '@testing-library/react'; -import { RecoilRoot, useRecoilValue } from 'recoil'; - -import { spreadsheetImportState } from '@/spreadsheet-import/states/spreadsheetImportState'; -import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; - -import { - personId, - query, - responseData, - variables, -} from '../__mocks__/useSpreadsheetPersonImport'; -import { useSpreadsheetPersonImport } from '../useSpreadsheetPersonImport'; - -jest.mock('uuid', () => ({ - v4: jest.fn(() => personId), -})); - -const mocks = [ - { - request: { - query, - variables, - }, - result: jest.fn(() => ({ - data: { - createPeople: responseData, - }, - })), - }, -]; - -const Wrapper = ({ children }: { children: ReactNode }) => ( - - - - {children} - - - -); - -const fakeCsv = () => { - const csvContent = 'firstname, lastname\nSheldon, Cooper'; - const blob = new Blob([csvContent], { type: 'text/csv' }); - return new File([blob], 'fakeData.csv', { type: 'text/csv' }); -}; - -describe('useSpreadsheetPersonImport', () => { - it('should work as expected', async () => { - const { result } = renderHook( - () => { - const spreadsheetImport = useRecoilValue(spreadsheetImportState); - const { openPersonSpreadsheetImport } = useSpreadsheetPersonImport(); - return { openPersonSpreadsheetImport, spreadsheetImport }; - }, - { - wrapper: Wrapper, - }, - ); - - const { spreadsheetImport, openPersonSpreadsheetImport } = result.current; - - expect(spreadsheetImport.isOpen).toBe(false); - expect(spreadsheetImport.options).toBeNull(); - - await act(async () => { - openPersonSpreadsheetImport(); - }); - - const { spreadsheetImport: updatedImport } = result.current; - - expect(updatedImport.isOpen).toBe(true); - expect(updatedImport.options).toHaveProperty('onSubmit'); - expect(updatedImport.options?.onSubmit).toBeInstanceOf(Function); - expect(updatedImport.options).toHaveProperty('fields'); - expect(Array.isArray(updatedImport.options?.fields)).toBe(true); - - act(() => { - updatedImport.options?.onSubmit( - { - validData: [ - { - firstName: 'Sheldon', - lastName: ' Cooper', - }, - ], - invalidData: [], - all: [ - { - firstName: 'Sheldon', - lastName: ' Cooper', - __index: 'cbc3985f-dde9-46d1-bae2-c124141700ac', - }, - ], - }, - fakeCsv(), - ); - }); - - await waitFor(() => { - expect(mocks[0].result).toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/twenty-front/src/modules/people/hooks/useSpreadsheetPersonImport.ts b/packages/twenty-front/src/modules/people/hooks/useSpreadsheetPersonImport.ts deleted file mode 100644 index e21ee67b05..0000000000 --- a/packages/twenty-front/src/modules/people/hooks/useSpreadsheetPersonImport.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { v4 } from 'uuid'; - -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords'; -import { Person } from '@/people/types/Person'; -import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport'; -import { SpreadsheetOptions } from '@/spreadsheet-import/types'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; - -import { fieldsForPerson } from '../utils/fieldsForPerson'; - -export type FieldPersonMapping = (typeof fieldsForPerson)[number]['key']; - -export const useSpreadsheetPersonImport = () => { - const { openSpreadsheetImport } = useSpreadsheetImport(); - const { enqueueSnackBar } = useSnackBar(); - - const { createManyRecords: createManyPeople } = useCreateManyRecords({ - objectNameSingular: CoreObjectNameSingular.Person, - }); - - const openPersonSpreadsheetImport = ( - options?: Omit< - SpreadsheetOptions, - 'fields' | 'isOpen' | 'onClose' - >, - ) => { - openSpreadsheetImport({ - ...options, - onSubmit: async (data) => { - // TODO: Add better type checking in spreadsheet import later - const createInputs = data.validData.map( - (person) => - ({ - id: v4(), - name: { - firstName: person.firstName as string | undefined, - lastName: person.lastName as string | undefined, - }, - email: person.email as string | undefined, - ...(person.linkedinUrl - ? { - linkedinLink: { - label: 'linkedinUrl', - url: person.linkedinUrl as string | undefined, - }, - } - : {}), - ...(person.xUrl - ? { - xLink: { - label: 'xUrl', - url: person.xUrl as string | undefined, - }, - } - : {}), - jobTitle: person.jobTitle as string | undefined, - phone: person.phone as string | undefined, - city: person.city as string | undefined, - }) as Person, - ); - - // TODO: abstract this part for any object - try { - await createManyPeople(createInputs); - } catch (error: any) { - enqueueSnackBar(error?.message || 'Something went wrong', { - variant: 'error', - }); - } - }, - fields: fieldsForPerson, - }); - }; - - return { openPersonSpreadsheetImport }; -}; diff --git a/packages/twenty-front/src/modules/people/utils/fieldsForPerson.tsx b/packages/twenty-front/src/modules/people/utils/fieldsForPerson.tsx deleted file mode 100644 index a2fe05a037..0000000000 --- a/packages/twenty-front/src/modules/people/utils/fieldsForPerson.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { isValidPhoneNumber } from 'libphonenumber-js'; - -import { Fields } from '@/spreadsheet-import/types'; -import { - IconBrandLinkedin, - IconBrandX, - IconBriefcase, - IconMail, - IconMap, - IconUser, -} from '@/ui/display/icon'; - -export const fieldsForPerson = [ - { - icon: IconUser, - label: 'Firstname', - key: 'firstName', - alternateMatches: ['first name', 'first', 'firstname'], - fieldType: { - type: 'input', - }, - example: 'Tim', - }, - { - icon: IconUser, - label: 'Lastname', - key: 'lastName', - alternateMatches: ['last name', 'last', 'lastname'], - fieldType: { - type: 'input', - }, - example: 'Cook', - }, - { - icon: IconMail, - label: 'Email', - key: 'email', - alternateMatches: ['email', 'mail'], - fieldType: { - type: 'input', - }, - example: 'tim@apple.dev', - }, - { - icon: IconBrandLinkedin, - label: 'Linkedin URL', - key: 'linkedinUrl', - alternateMatches: ['linkedIn', 'linkedin', 'linkedin url'], - fieldType: { - type: 'input', - }, - example: 'https://www.linkedin.com/in/timcook', - }, - { - icon: IconBrandX, - label: 'X URL', - key: 'xUrl', - alternateMatches: ['x', 'x url'], - fieldType: { - type: 'input', - }, - example: 'https://x.com/tim_cook', - }, - { - icon: IconBriefcase, - label: 'Job title', - key: 'jobTitle', - alternateMatches: ['job', 'job title'], - fieldType: { - type: 'input', - }, - example: 'CEO', - }, - { - icon: IconBriefcase, - label: 'Phone', - key: 'phone', - fieldType: { - type: 'input', - }, - example: '+1234567890', - validations: [ - { - rule: 'function', - isValid: (value: string) => isValidPhoneNumber(value), - errorMessage: 'phone is not valid', - level: 'error', - }, - ], - }, - { - icon: IconMap, - label: 'City', - key: 'city', - fieldType: { - type: 'input', - }, - example: 'Seattle', - }, -] as Fields; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx index 44a3c43e9f..b0c69912eb 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx @@ -129,9 +129,8 @@ export const MatchColumnSelect = ({ {options?.map((option) => ( - <> + handleChange(option)} disabled={ @@ -152,9 +151,11 @@ export const MatchColumnSelect = ({ />, document.body, )} - + ))} - {options?.length === 0 && } + {options?.length === 0 && ( + + )} ,