Refactor spreadsheet import (#11250)

Mostly renaming objects to avoid conflicts (it was painful because names
were too generic so you could cmd+replace easily)

Also refactoring `useBuildAvailableFieldsForImport`
This commit is contained in:
Félix Malfait
2025-03-28 07:56:51 +01:00
committed by GitHub
parent 9af2628264
commit e9e33c4d29
84 changed files with 960 additions and 916 deletions

View File

@ -1,45 +1,45 @@
import {
Field,
ImportedStructuredRow,
Info,
RowHook,
TableHook,
SpreadsheetImportField,
SpreadsheetImportInfo,
SpreadsheetImportRowHook,
SpreadsheetImportTableHook,
} from '@/spreadsheet-import/types';
import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations';
import { FieldMetadataType } from 'twenty-shared/types';
describe('addErrorsAndRunHooks', () => {
type FullData = ImportedStructuredRow<'name' | 'age' | 'country'>;
const requiredField: Field<'name'> = {
const requiredField: SpreadsheetImportField<'name'> = {
key: 'name',
label: 'Name',
fieldValidationDefinitions: [{ rule: 'required' }],
icon: null,
Icon: null,
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.TEXT,
};
const regexField: Field<'age'> = {
const regexField: SpreadsheetImportField<'age'> = {
key: 'age',
label: 'Age',
fieldValidationDefinitions: [
{ rule: 'regex', value: '\\d+', errorMessage: 'Regex error' },
],
icon: null,
Icon: null,
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.NUMBER,
};
const uniqueField: Field<'country'> = {
const uniqueField: SpreadsheetImportField<'country'> = {
key: 'country',
label: 'Country',
fieldValidationDefinitions: [{ rule: 'unique' }],
icon: null,
Icon: null,
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.SELECT,
};
const functionValidationFieldTrue: Field<'email'> = {
const functionValidationFieldTrue: SpreadsheetImportField<'email'> = {
key: 'email',
label: 'Email',
fieldValidationDefinitions: [
@ -49,12 +49,12 @@ describe('addErrorsAndRunHooks', () => {
errorMessage: 'Field is invalid',
},
],
icon: null,
Icon: null,
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.EMAILS,
};
const functionValidationFieldFalse: Field<'email'> = {
const functionValidationFieldFalse: SpreadsheetImportField<'email'> = {
key: 'email',
label: 'Email',
fieldValidationDefinitions: [
@ -64,7 +64,7 @@ describe('addErrorsAndRunHooks', () => {
errorMessage: 'Field is invalid',
},
],
icon: null,
Icon: null,
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.EMAILS,
};
@ -88,24 +88,43 @@ describe('addErrorsAndRunHooks', () => {
dataWithoutNameAndInvalidAge,
];
const basicError: Info = { message: 'Field is invalid', level: 'error' };
const nameError: Info = { message: 'Name Error', level: 'error' };
const ageError: Info = { message: 'Age Error', level: 'error' };
const regexError: Info = { message: 'Regex error', level: 'error' };
const requiredError: Info = { message: 'Field is required', level: 'error' };
const duplicatedError: Info = {
const basicError: SpreadsheetImportInfo = {
message: 'Field is invalid',
level: 'error',
};
const nameError: SpreadsheetImportInfo = {
message: 'Name Error',
level: 'error',
};
const ageError: SpreadsheetImportInfo = {
message: 'Age Error',
level: 'error',
};
const regexError: SpreadsheetImportInfo = {
message: 'Regex error',
level: 'error',
};
const requiredError: SpreadsheetImportInfo = {
message: 'Field is required',
level: 'error',
};
const duplicatedError: SpreadsheetImportInfo = {
message: 'Field must be unique',
level: 'error',
};
const rowHook: RowHook<'name' | 'age'> = jest.fn((row, addError) => {
addError('name', nameError);
return row;
});
const tableHook: TableHook<'name' | 'age'> = 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(

View File

@ -1,11 +1,11 @@
import { Field } from '@/spreadsheet-import/types';
import { SpreadsheetImportField } from '@/spreadsheet-import/types';
import { findMatch } from '@/spreadsheet-import/utils/findMatch';
import { FieldMetadataType } from 'twenty-shared/types';
describe('findMatch', () => {
const defaultField: Field<'defaultField'> = {
const defaultField: SpreadsheetImportField<'defaultField'> = {
key: 'defaultField',
icon: null,
Icon: null,
label: 'label',
fieldType: {
type: 'input',
@ -14,9 +14,9 @@ describe('findMatch', () => {
alternateMatches: ['Full Name', 'First Name'],
};
const secondaryField: Field<'secondaryField'> = {
const secondaryField: SpreadsheetImportField<'secondaryField'> = {
key: 'secondaryField',
icon: null,
Icon: null,
label: 'label',
fieldType: {
type: 'input',

View File

@ -1,59 +1,62 @@
import {
Column,
ColumnType,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { Field, FieldValidationDefinition } from '@/spreadsheet-import/types';
SpreadsheetImportField,
SpreadsheetImportFieldValidationDefinition,
} from '@/spreadsheet-import/types';
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields';
import { FieldMetadataType } from 'twenty-shared/types';
const nameField: Field<'Name'> = {
const nameField: SpreadsheetImportField<'Name'> = {
key: 'Name',
label: 'Name',
icon: null,
Icon: null,
fieldType: {
type: 'input',
},
fieldMetadataType: FieldMetadataType.TEXT,
};
const ageField: Field<'Age'> = {
const ageField: SpreadsheetImportField<'Age'> = {
key: 'Age',
label: 'Age',
icon: null,
Icon: null,
fieldType: {
type: 'input',
},
fieldMetadataType: FieldMetadataType.NUMBER,
};
const validations: FieldValidationDefinition[] = [{ rule: 'required' }];
const nameFieldWithValidations: Field<'Name'> = {
const validations: SpreadsheetImportFieldValidationDefinition[] = [
{ rule: 'required' },
];
const nameFieldWithValidations: SpreadsheetImportField<'Name'> = {
...nameField,
fieldValidationDefinitions: validations,
};
const ageFieldWithValidations: Field<'Age'> = {
const ageFieldWithValidations: SpreadsheetImportField<'Age'> = {
...ageField,
fieldValidationDefinitions: validations,
};
type ColumnValues = 'Name' | 'Age';
const nameColumn: Column<ColumnValues> = {
type: ColumnType.matched,
const nameColumn: SpreadsheetColumn<ColumnValues> = {
type: SpreadsheetColumnType.matched,
index: 0,
header: '',
value: 'Name',
};
const ageColumn: Column<ColumnValues> = {
type: ColumnType.matched,
const ageColumn: SpreadsheetColumn<ColumnValues> = {
type: SpreadsheetColumnType.matched,
index: 0,
header: '',
value: 'Age',
};
const extraColumn: Column<ColumnValues> = {
type: ColumnType.matched,
const extraColumn: SpreadsheetColumn<ColumnValues> = {
type: SpreadsheetColumnType.matched,
index: 0,
header: '',
value: 'Age',

View File

@ -1,11 +1,11 @@
import { Field } from '@/spreadsheet-import/types';
import { SpreadsheetImportField } from '@/spreadsheet-import/types';
import { generateExampleRow } from '@/spreadsheet-import/utils/generateExampleRow';
import { FieldMetadataType } from 'twenty-shared/types';
describe('generateExampleRow', () => {
const defaultField: Field<'defaultField'> = {
const defaultField: SpreadsheetImportField<'defaultField'> = {
key: 'defaultField',
icon: null,
Icon: null,
label: 'label',
fieldType: {
type: 'input',
@ -14,7 +14,7 @@ describe('generateExampleRow', () => {
};
it('should generate an example row from input field type', () => {
const fields: Field<'defaultField'>[] = [defaultField];
const fields: SpreadsheetImportField<'defaultField'>[] = [defaultField];
const result = generateExampleRow(fields);
@ -22,7 +22,7 @@ describe('generateExampleRow', () => {
});
it('should generate an example row from checkbox field type', () => {
const fields: Field<'defaultField'>[] = [
const fields: SpreadsheetImportField<'defaultField'>[] = [
{
...defaultField,
fieldType: { type: 'checkbox' },
@ -36,7 +36,7 @@ describe('generateExampleRow', () => {
});
it('should generate an example row from select field type', () => {
const fields: Field<'defaultField'>[] = [
const fields: SpreadsheetImportField<'defaultField'>[] = [
{
...defaultField,
fieldType: { type: 'select', options: [] },
@ -50,7 +50,7 @@ describe('generateExampleRow', () => {
});
it('should generate an example row with provided example values for fields', () => {
const fields: Field<'defaultField'>[] = [
const fields: SpreadsheetImportField<'defaultField'>[] = [
{
...defaultField,
example: 'Example',

View File

@ -1,4 +1,4 @@
import { Field } from '@/spreadsheet-import/types';
import { SpreadsheetImportField } from '@/spreadsheet-import/types';
import { getFieldOptions } from '@/spreadsheet-import/utils/getFieldOptions';
import { FieldMetadataType } from 'twenty-shared/types';
@ -17,10 +17,10 @@ describe('getFieldOptions', () => {
value: 'Three',
},
];
const fields: Field<'Options' | 'Name'>[] = [
const fields: SpreadsheetImportField<'Options' | 'Name'>[] = [
{
key: 'Options',
icon: null,
Icon: null,
label: 'options',
fieldType: {
type: 'select',
@ -30,7 +30,7 @@ describe('getFieldOptions', () => {
},
{
key: 'Name',
icon: null,
Icon: null,
label: 'name',
fieldType: {
type: 'input',

View File

@ -1,49 +1,52 @@
import {
Column,
ColumnType,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { Field } from '@/spreadsheet-import/types';
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: Column<string>[] = [
{ index: 0, header: 'Name', type: ColumnType.matched, value: 'Name' },
const columns: SpreadsheetColumn<string>[] = [
{
index: 0,
header: 'Name',
type: SpreadsheetColumnType.matched,
value: 'Name',
},
{
index: 1,
header: 'Location',
type: ColumnType.matched,
type: SpreadsheetColumnType.matched,
value: 'Location',
},
{
index: 2,
header: 'Age',
type: ColumnType.matched,
type: SpreadsheetColumnType.matched,
value: 'Age',
},
];
const fields: Field<string>[] = [
const fields: SpreadsheetImportField<string>[] = [
{
key: 'Name',
label: 'Name',
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.TEXT,
icon: null,
Icon: null,
},
{
key: 'Location',
label: 'Location',
fieldType: { type: 'select', options: [] },
fieldMetadataType: FieldMetadataType.POSITION,
icon: null,
Icon: null,
},
{
key: 'Age',
label: 'Age',
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.NUMBER,
icon: null,
Icon: null,
},
];
@ -57,11 +60,16 @@ describe('getMatchedColumns', () => {
it('should return matched columns for each field', () => {
const result = getMatchedColumns(columns, fields, data, autoMapDistance);
expect(result).toEqual([
{ index: 0, header: 'Name', type: ColumnType.matched, value: 'Name' },
{
index: 0,
header: 'Name',
type: SpreadsheetColumnType.matched,
value: 'Name',
},
{
index: 1,
header: 'Location',
type: ColumnType.matchedSelect,
type: SpreadsheetColumnType.matchedSelect,
value: 'Location',
matchedOptions: [
{
@ -72,18 +80,33 @@ describe('getMatchedColumns', () => {
},
],
},
{ index: 2, header: 'Age', type: ColumnType.matched, value: 'Age' },
{
index: 2,
header: 'Age',
type: SpreadsheetColumnType.matched,
value: 'Age',
},
]);
});
it('should handle columns with duplicate values by choosing the closest match', () => {
const columnsWithDuplicates: Column<string>[] = [
{ index: 0, header: 'Name', type: ColumnType.matched, value: 'Name' },
{ index: 1, header: 'Name', type: ColumnType.matched, value: 'Name' },
const columnsWithDuplicates: SpreadsheetColumn<string>[] = [
{
index: 0,
header: 'Name',
type: SpreadsheetColumnType.matched,
value: 'Name',
},
{
index: 1,
header: 'Name',
type: SpreadsheetColumnType.matched,
value: 'Name',
},
{
index: 2,
header: 'Location',
type: ColumnType.matched,
type: SpreadsheetColumnType.matched,
value: 'Location',
},
];
@ -98,12 +121,12 @@ describe('getMatchedColumns', () => {
expect(result[0]).toEqual({
index: 0,
header: 'Name',
type: ColumnType.empty,
type: SpreadsheetColumnType.empty,
});
expect(result[1]).toEqual({
index: 1,
header: 'Name',
type: ColumnType.matched,
type: SpreadsheetColumnType.matched,
value: 'Name',
});
});
@ -114,20 +137,20 @@ describe('getMatchedColumns', () => {
['Alice', 'Los Angeles', '25'],
];
const unmatchedFields: Field<string>[] = [
const unmatchedFields: SpreadsheetImportField<string>[] = [
{
key: 'Hobby',
label: 'Hobby',
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.TEXT,
icon: null,
Icon: null,
},
{
key: 'Interest',
label: 'Interest',
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.TEXT,
icon: null,
Icon: null,
},
];

View File

@ -1,37 +1,46 @@
import {
Column,
ColumnType,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { Field } from '@/spreadsheet-import/types';
import { SpreadsheetImportField } from '@/spreadsheet-import/types';
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { normalizeTableData } from '@/spreadsheet-import/utils/normalizeTableData';
import { FieldMetadataType } from 'twenty-shared/types';
describe('normalizeTableData', () => {
const columns: Column<string>[] = [
{ index: 0, header: 'Name', type: ColumnType.matched, value: 'name' },
{ index: 1, header: 'Age', type: ColumnType.matched, value: 'age' },
const columns: SpreadsheetColumn<string>[] = [
{
index: 0,
header: 'Name',
type: SpreadsheetColumnType.matched,
value: 'name',
},
{
index: 1,
header: 'Age',
type: SpreadsheetColumnType.matched,
value: 'age',
},
{
index: 2,
header: 'Active',
type: ColumnType.matchedCheckbox,
type: SpreadsheetColumnType.matchedCheckbox,
value: 'active',
},
];
const fields: Field<string>[] = [
const fields: SpreadsheetImportField<string>[] = [
{
key: 'name',
label: 'Name',
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.TEXT,
icon: null,
Icon: null,
},
{
key: 'age',
label: 'Age',
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.NUMBER,
icon: null,
Icon: null,
},
{
key: 'active',
@ -40,7 +49,7 @@ describe('normalizeTableData', () => {
type: 'checkbox',
},
fieldMetadataType: FieldMetadataType.BOOLEAN,
icon: null,
Icon: null,
},
];
@ -61,16 +70,16 @@ describe('normalizeTableData', () => {
});
it('should normalize matchedCheckbox values and handle booleanMatches', () => {
const columns: Column<string>[] = [
const columns: SpreadsheetColumn<string>[] = [
{
index: 0,
header: 'Active',
type: ColumnType.matchedCheckbox,
type: SpreadsheetColumnType.matchedCheckbox,
value: 'active',
},
];
const fields: Field<string>[] = [
const fields: SpreadsheetImportField<string>[] = [
{
key: 'active',
label: 'Active',
@ -79,7 +88,7 @@ describe('normalizeTableData', () => {
booleanMatches: { yes: true, no: false },
},
fieldMetadataType: FieldMetadataType.BOOLEAN,
icon: null,
Icon: null,
},
];
@ -91,11 +100,11 @@ describe('normalizeTableData', () => {
});
it('should map matchedSelect and matchedSelectOptions values correctly', () => {
const columns: Column<string>[] = [
const columns: SpreadsheetColumn<string>[] = [
{
index: 0,
header: 'Number',
type: ColumnType.matchedSelect,
type: SpreadsheetColumnType.matchedSelect,
value: 'number',
matchedOptions: [
{ entry: 'One', value: '1' },
@ -104,7 +113,7 @@ describe('normalizeTableData', () => {
},
];
const fields: Field<string>[] = [
const fields: SpreadsheetImportField<string>[] = [
{
key: 'number',
label: 'Number',
@ -116,7 +125,7 @@ describe('normalizeTableData', () => {
],
},
fieldMetadataType: FieldMetadataType.SELECT,
icon: null,
Icon: null,
},
];
@ -132,9 +141,9 @@ describe('normalizeTableData', () => {
});
it('should handle empty and ignored columns', () => {
const columns: Column<string>[] = [
{ index: 0, header: 'Empty', type: ColumnType.empty },
{ index: 1, header: 'Ignored', type: ColumnType.ignored },
const columns: SpreadsheetColumn<string>[] = [
{ index: 0, header: 'Empty', type: SpreadsheetColumnType.empty },
{ index: 1, header: 'Ignored', type: SpreadsheetColumnType.ignored },
];
const rawData = [['Value1', 'Value2']];
@ -145,11 +154,11 @@ describe('normalizeTableData', () => {
});
it('should handle unrecognized column types and return empty object', () => {
const columns: Column<string>[] = [
const columns: SpreadsheetColumns<string> = [
{
index: 0,
header: 'Unrecognized',
type: 'Unknown' as unknown as ColumnType.matched,
type: 'Unknown' as unknown as SpreadsheetColumnType.matched,
value: '',
},
];

View File

@ -1,24 +1,22 @@
import {
Column,
ColumnType,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { Field } from '@/spreadsheet-import/types';
import { SpreadsheetImportField } from '@/spreadsheet-import/types';
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { setColumn } from '@/spreadsheet-import/utils/setColumn';
import { FieldMetadataType } from 'twenty-shared/types';
describe('setColumn', () => {
const defaultField: Field<'Name'> = {
icon: null,
const defaultField: SpreadsheetImportField<'Name'> = {
Icon: null,
label: 'label',
key: 'Name',
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.TEXT,
};
const oldColumn: Column<'oldValue'> = {
const oldColumn: SpreadsheetColumn<'oldValue'> = {
index: 0,
header: 'Name',
type: ColumnType.matched,
type: SpreadsheetColumnType.matched,
value: 'oldValue',
};
@ -29,7 +27,7 @@ describe('setColumn', () => {
type: 'select',
options: [{ value: 'John' }, { value: 'Alice' }],
},
} as Field<'Name'>;
} as SpreadsheetImportField<'Name'>;
const data = [['John'], ['Alice']];
const result = setColumn(oldColumn, field, data);
@ -37,7 +35,7 @@ describe('setColumn', () => {
expect(result).toEqual({
index: 0,
header: 'Name',
type: ColumnType.matchedSelectOptions,
type: SpreadsheetColumnType.matchedSelectOptions,
value: 'Name',
matchedOptions: [
{
@ -56,14 +54,14 @@ describe('setColumn', () => {
const field = {
...defaultField,
fieldType: { type: 'checkbox' },
} as Field<'Name'>;
} as SpreadsheetImportField<'Name'>;
const result = setColumn(oldColumn, field);
expect(result).toEqual({
index: 0,
header: 'Name',
type: ColumnType.matchedCheckbox,
type: SpreadsheetColumnType.matchedCheckbox,
value: 'Name',
});
});
@ -72,14 +70,14 @@ describe('setColumn', () => {
const field = {
...defaultField,
fieldType: { type: 'input' },
} as Field<'Name'>;
} as SpreadsheetImportField<'Name'>;
const result = setColumn(oldColumn, field);
expect(result).toEqual({
index: 0,
header: 'Name',
type: ColumnType.matched,
type: SpreadsheetColumnType.matched,
value: 'Name',
});
});
@ -88,14 +86,14 @@ describe('setColumn', () => {
const field = {
...defaultField,
fieldType: { type: 'unknown' },
} as unknown as Field<'Name'>;
} as unknown as SpreadsheetImportField<'Name'>;
const result = setColumn(oldColumn, field);
expect(result).toEqual({
index: 0,
header: 'Name',
type: ColumnType.empty,
type: SpreadsheetColumnType.empty,
});
});
});

View File

@ -1,22 +1,20 @@
import {
Column,
ColumnType,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { setIgnoreColumn } from '@/spreadsheet-import/utils/setIgnoreColumn';
describe('setIgnoreColumn', () => {
it('should return a column with type "ignored"', () => {
const column: Column<'John'> = {
const column: SpreadsheetColumn<'John'> = {
index: 0,
header: 'Name',
type: ColumnType.matched,
type: SpreadsheetColumnType.matched,
value: 'John',
};
const result = setIgnoreColumn(column);
expect(result).toEqual({
index: 0,
header: 'Name',
type: ColumnType.ignored,
type: SpreadsheetColumnType.ignored,
});
});
});

View File

@ -1,15 +1,13 @@
import {
Column,
ColumnType,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { setSubColumn } from '@/spreadsheet-import/utils/setSubColumn';
describe('setSubColumn', () => {
it('should return a matchedSelectColumn with updated matchedOptions', () => {
const oldColumn: Column<'John' | ''> = {
const oldColumn: SpreadsheetColumn<'John' | ''> = {
index: 0,
header: 'Name',
type: ColumnType.matchedSelect,
type: SpreadsheetColumnType.matchedSelect,
matchedOptions: [
{ entry: 'Name1', value: 'John' },
{ entry: 'Name2', value: '' },
@ -24,7 +22,7 @@ describe('setSubColumn', () => {
expect(result).toEqual({
index: 0,
header: 'Name',
type: ColumnType.matchedSelect,
type: SpreadsheetColumnType.matchedSelect,
matchedOptions: [
{ entry: 'Name1', value: 'John Doe' },
{ entry: 'Name2', value: '' },
@ -34,10 +32,10 @@ describe('setSubColumn', () => {
});
it('should return a matchedSelectOptionsColumn with updated matchedOptions', () => {
const oldColumn: Column<'John' | 'Jane'> = {
const oldColumn: SpreadsheetColumn<'John' | 'Jane'> = {
index: 0,
header: 'Name',
type: ColumnType.matchedSelectOptions,
type: SpreadsheetColumnType.matchedSelectOptions,
matchedOptions: [
{ entry: 'Name1', value: 'John' },
{ entry: 'Name2', value: 'Jane' },
@ -52,7 +50,7 @@ describe('setSubColumn', () => {
expect(result).toEqual({
index: 0,
header: 'Name',
type: ColumnType.matchedSelectOptions,
type: SpreadsheetColumnType.matchedSelectOptions,
matchedOptions: [
{ entry: 'Name1', value: 'John Doe' },
{ entry: 'Name2', value: 'Jane' },

View File

@ -6,24 +6,28 @@ import {
ImportedStructuredRowMetadata,
} from '@/spreadsheet-import/steps/components/ValidationStep/types';
import {
Fields,
ImportedStructuredRow,
Info,
RowHook,
TableHook,
SpreadsheetImportFields,
SpreadsheetImportInfo,
SpreadsheetImportRowHook,
SpreadsheetImportTableHook,
} from '@/spreadsheet-import/types';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { isDefined } from 'twenty-shared/utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const addErrorsAndRunHooks = <T extends string>(
data: (ImportedStructuredRow<T> & Partial<ImportedStructuredRowMetadata>)[],
fields: Fields<T>,
rowHook?: RowHook<T>,
tableHook?: TableHook<T>,
fields: SpreadsheetImportFields<T>,
rowHook?: SpreadsheetImportRowHook<T>,
tableHook?: SpreadsheetImportTableHook<T>,
): (ImportedStructuredRow<T> & ImportedStructuredRowMetadata)[] => {
const errors: Errors = {};
const addHookError = (rowIndex: number, fieldKey: T, error: Info) => {
const addHookError = (
rowIndex: number,
fieldKey: T,
error: SpreadsheetImportInfo,
) => {
errors[rowIndex] = {
...errors[rowIndex],
[fieldKey]: error,

View File

@ -1,7 +1,6 @@
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
import lavenstein from 'js-levenshtein';
import { Fields } from '@/spreadsheet-import/types';
type AutoMatchAccumulator<T> = {
distance: number;
value: T;
@ -9,7 +8,7 @@ type AutoMatchAccumulator<T> = {
export const findMatch = <T extends string>(
header: string,
fields: Fields<T>,
fields: SpreadsheetImportFields<T>,
autoMapDistance: number,
): T | undefined => {
const smallestValue = fields.reduce<AutoMatchAccumulator<T>>((acc, field) => {

View File

@ -1,9 +1,9 @@
import { Columns } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { Fields } from '@/spreadsheet-import/types';
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
export const findUnmatchedRequiredFields = <T extends string>(
fields: Fields<T>,
columns: Columns<T>,
fields: SpreadsheetImportFields<T>,
columns: SpreadsheetColumns<T>,
) =>
fields
.filter((field) =>

View File

@ -1,13 +1,21 @@
import { Field, Fields } from '@/spreadsheet-import/types';
import {
SpreadsheetImportField,
SpreadsheetImportFields,
} from '@/spreadsheet-import/types';
const titleMap: Record<Field<string>['fieldType']['type'], string> = {
const titleMap: Record<
SpreadsheetImportField<string>['fieldType']['type'],
string
> = {
checkbox: 'Boolean',
select: 'Options',
multiSelect: 'Options',
input: 'Text',
};
export const generateExampleRow = <T extends string>(fields: Fields<T>) => [
export const generateExampleRow = <T extends string>(
fields: SpreadsheetImportFields<T>,
) => [
fields.reduce(
(acc, field) => {
acc[field.key as T] = field.example || titleMap[field.fieldType.type];

View File

@ -1,7 +1,7 @@
import { Fields } from '@/spreadsheet-import/types';
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
export const getFieldOptions = <T extends string>(
fields: Fields<T>,
fields: SpreadsheetImportFields<T>,
fieldKey: string,
) => {
const field = fields.find(({ key }) => fieldKey === key);

View File

@ -1,26 +1,29 @@
import lavenstein from 'js-levenshtein';
import {
Column,
Columns,
MatchColumnsStepProps,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { Field, Fields } from '@/spreadsheet-import/types';
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';
import { isDefined } from 'twenty-shared/utils';
export const getMatchedColumns = <T extends string>(
columns: Columns<T>,
fields: Fields<T>,
columns: SpreadsheetColumns<T>,
fields: SpreadsheetImportFields<T>,
data: MatchColumnsStepProps['data'],
autoMapDistance: number,
) =>
columns.reduce<Column<T>[]>((arr, column) => {
columns.reduce<SpreadsheetColumn<T>[]>((arr, column) => {
const autoMatch = findMatch(column.header, fields, autoMapDistance);
if (isDefined(autoMatch)) {
const field = fields.find((field) => field.key === autoMatch) as Field<T>;
const field = fields.find(
(field) => field.key === autoMatch,
) as SpreadsheetImportField<T>;
const duplicateIndex = arr.findIndex(
(column) => 'value' in column && column.value === field.key,
);

View File

@ -1,26 +1,24 @@
import {
Columns,
ColumnType,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import {
Fields,
ImportedRow,
ImportedStructuredRow,
SpreadsheetImportFields,
} from '@/spreadsheet-import/types';
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { isDefined } from 'twenty-shared/utils';
import { z } from 'zod';
import { normalizeCheckboxValue } from './normalizeCheckboxValue';
import { isDefined } from 'twenty-shared/utils';
export const normalizeTableData = <T extends string>(
columns: Columns<T>,
columns: SpreadsheetColumns<T>,
data: ImportedRow[],
fields: Fields<T>,
fields: SpreadsheetImportFields<T>,
) =>
data.map((row) =>
columns.reduce((acc, column, index) => {
const curr = row[index];
switch (column.type) {
case ColumnType.matchedCheckbox: {
case SpreadsheetColumnType.matchedCheckbox: {
const field = fields.find((field) => field.key === column.value);
if (!field) {
@ -49,12 +47,12 @@ export const normalizeTableData = <T extends string>(
}
return acc;
}
case ColumnType.matched: {
case SpreadsheetColumnType.matched: {
acc[column.value] = curr === '' ? undefined : curr;
return acc;
}
case ColumnType.matchedSelect:
case ColumnType.matchedSelectOptions: {
case SpreadsheetColumnType.matchedSelect:
case SpreadsheetColumnType.matchedSelectOptions: {
const field = fields.find((field) => field.key === column.value);
if (!field) {
@ -96,8 +94,8 @@ export const normalizeTableData = <T extends string>(
}
return acc;
}
case ColumnType.empty:
case ColumnType.ignored: {
case SpreadsheetColumnType.empty:
case SpreadsheetColumnType.ignored: {
return acc;
}
default:

View File

@ -1,25 +1,23 @@
import {
Column,
ColumnType,
MatchColumnsStepProps,
MatchedOptions,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { Field } from '@/spreadsheet-import/types';
import { MatchColumnsStepProps } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { SpreadsheetImportField } from '@/spreadsheet-import/types';
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
import { z } from 'zod';
import { uniqueEntries } from './uniqueEntries';
export const setColumn = <T extends string>(
oldColumn: Column<T>,
field?: Field<T>,
oldColumn: SpreadsheetColumn<T>,
field?: SpreadsheetImportField<T>,
data?: MatchColumnsStepProps['data'],
): Column<T> => {
): SpreadsheetColumn<T> => {
if (field?.fieldType.type === 'select') {
const fieldOptions = field.fieldType.options;
const uniqueData = uniqueEntries(
data || [],
oldColumn.index,
) as MatchedOptions<T>[];
) as SpreadsheetMatchedOptions<T>[];
const matchedOptions = uniqueData.map((record) => {
const value = fieldOptions.find(
@ -28,8 +26,8 @@ export const setColumn = <T extends string>(
fieldOption.label === record.entry,
)?.value;
return value
? ({ ...record, value } as MatchedOptions<T>)
: (record as MatchedOptions<T>);
? ({ ...record, value } as SpreadsheetMatchedOptions<T>)
: (record as SpreadsheetMatchedOptions<T>);
});
const allMatched =
matchedOptions.filter((o) => o.value).length === uniqueData?.length;
@ -37,8 +35,8 @@ export const setColumn = <T extends string>(
return {
...oldColumn,
type: allMatched
? ColumnType.matchedSelectOptions
: ColumnType.matchedSelect,
? SpreadsheetColumnType.matchedSelectOptions
: SpreadsheetColumnType.matchedSelect,
value: field.key,
matchedOptions,
};
@ -69,8 +67,8 @@ export const setColumn = <T extends string>(
fieldOption.value === entry || fieldOption.label === entry,
)?.value;
return value
? ({ entry, value } as MatchedOptions<T>)
: ({ entry } as MatchedOptions<T>);
? ({ entry, value } as SpreadsheetMatchedOptions<T>)
: ({ entry } as SpreadsheetMatchedOptions<T>);
});
const areAllMatched =
matchedOptions.filter((option) => option.value).length ===
@ -79,8 +77,8 @@ export const setColumn = <T extends string>(
return {
...oldColumn,
type: areAllMatched
? ColumnType.matchedSelectOptions
: ColumnType.matchedSelect,
? SpreadsheetColumnType.matchedSelectOptions
: SpreadsheetColumnType.matchedSelect,
value: field.key,
matchedOptions,
};
@ -89,7 +87,7 @@ export const setColumn = <T extends string>(
if (field?.fieldType.type === 'checkbox') {
return {
index: oldColumn.index,
type: ColumnType.matchedCheckbox,
type: SpreadsheetColumnType.matchedCheckbox,
value: field.key,
header: oldColumn.header,
};
@ -98,7 +96,7 @@ export const setColumn = <T extends string>(
if (field?.fieldType.type === 'input') {
return {
index: oldColumn.index,
type: ColumnType.matched,
type: SpreadsheetColumnType.matched,
value: field.key,
header: oldColumn.header,
};
@ -107,6 +105,6 @@ export const setColumn = <T extends string>(
return {
index: oldColumn.index,
header: oldColumn.header,
type: ColumnType.empty,
type: SpreadsheetColumnType.empty,
};
};

View File

@ -1,13 +1,11 @@
import {
Column,
ColumnType,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
export const setIgnoreColumn = <T extends string>({
header,
index,
}: Column<T>): Column<T> => ({
}: SpreadsheetColumn<T>): SpreadsheetColumn<T> => ({
header,
index,
type: ColumnType.ignored,
type: SpreadsheetColumnType.ignored,
});

View File

@ -1,30 +1,34 @@
import {
ColumnType,
MatchedOptions,
MatchedSelectColumn,
MatchedSelectOptionsColumn,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
SpreadsheetMatchedSelectColumn,
SpreadsheetMatchedSelectOptionsColumn,
} from '@/spreadsheet-import/types/SpreadsheetColumn';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
export const setSubColumn = <T>(
oldColumn: MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T>,
oldColumn:
| SpreadsheetMatchedSelectColumn<T>
| SpreadsheetMatchedSelectOptionsColumn<T>,
entry: string,
value: string,
): MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T> => {
):
| SpreadsheetMatchedSelectColumn<T>
| SpreadsheetMatchedSelectOptionsColumn<T> => {
const options = oldColumn.matchedOptions.map((option) =>
option.entry === entry ? { ...option, value } : option,
);
const allMathced = options.every(({ value }) => !!value);
if (allMathced) {
const allMatched = options.every(({ value }) => !!value);
if (allMatched) {
return {
...oldColumn,
matchedOptions: options as MatchedOptions<T>[],
type: ColumnType.matchedSelectOptions,
matchedOptions: options as SpreadsheetMatchedOptions<T>[],
type: SpreadsheetColumnType.matchedSelectOptions,
};
} else {
return {
...oldColumn,
matchedOptions: options as MatchedOptions<T>[],
type: ColumnType.matchedSelect,
matchedOptions: options as SpreadsheetMatchedOptions<T>[],
type: SpreadsheetColumnType.matchedSelect,
};
}
};

View File

@ -1,14 +1,12 @@
import uniqBy from 'lodash.uniqby';
import {
MatchColumnsStepProps,
MatchedOptions,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { MatchColumnsStepProps } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
export const uniqueEntries = <T extends string>(
data: MatchColumnsStepProps['data'],
index: number,
): Partial<MatchedOptions<T>>[] =>
): Partial<SpreadsheetMatchedOptions<T>>[] =>
uniqBy(
data.map((row) => ({ entry: row[index] })),
'entry',