This reverts commit cc71394863.
Regression introduced in https://github.com/twentyhq/twenty/pull/13213
The import/export use an upsert logic and when it goes through the
"update" path it fails due to the connect not being implemented yet
(should be in https://github.com/twentyhq/core-team-issues/issues/1230)
---------
Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
+28
-29
@@ -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],
|
||||
);
|
||||
|
||||
|
||||
+93
@@ -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();
|
||||
});
|
||||
});
|
||||
+9
-11
@@ -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<ColumnValues> = {
|
||||
type: SpreadsheetColumnType.matched,
|
||||
index: 0,
|
||||
header: '',
|
||||
value: 'Name',
|
||||
};
|
||||
|
||||
const ageColumn: SpreadsheetColumn = {
|
||||
const ageColumn: SpreadsheetColumn<ColumnValues> = {
|
||||
type: SpreadsheetColumnType.matched,
|
||||
index: 0,
|
||||
header: '',
|
||||
value: 'Age',
|
||||
};
|
||||
|
||||
const extraColumn: SpreadsheetColumn = {
|
||||
const extraColumn: SpreadsheetColumn<ColumnValues> = {
|
||||
type: SpreadsheetColumnType.matched,
|
||||
index: 0,
|
||||
header: '',
|
||||
|
||||
+1
-5
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+166
@@ -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<string>[] = [
|
||||
{
|
||||
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<string>[] = [
|
||||
{
|
||||
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<string>[] = [
|
||||
{
|
||||
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<string>[] = [
|
||||
{
|
||||
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);
|
||||
});
|
||||
});
|
||||
+11
-13
@@ -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<string>[] = [
|
||||
{
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
@@ -27,7 +27,7 @@ describe('normalizeTableData', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const fields = [
|
||||
const fields: SpreadsheetImportField<string>[] = [
|
||||
{
|
||||
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<string>[] = [
|
||||
{
|
||||
index: 0,
|
||||
header: 'Active',
|
||||
@@ -79,7 +79,7 @@ describe('normalizeTableData', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const fields = [
|
||||
const fields: SpreadsheetImportField<string>[] = [
|
||||
{
|
||||
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<string>[] = [
|
||||
{
|
||||
index: 0,
|
||||
header: 'Number',
|
||||
@@ -115,7 +113,7 @@ describe('normalizeTableData', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const fields = [
|
||||
const fields: SpreadsheetImportField<string>[] = [
|
||||
{
|
||||
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<string>[] = [
|
||||
{ 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<string> = [
|
||||
{
|
||||
index: 0,
|
||||
header: 'Unrecognized',
|
||||
|
||||
+7
-7
@@ -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);
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
@@ -15,17 +15,17 @@ import {
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export const addErrorsAndRunHooks = (
|
||||
data: (ImportedStructuredRow & Partial<ImportedStructuredRowMetadata>)[],
|
||||
fields: SpreadsheetImportFields,
|
||||
rowHook?: SpreadsheetImportRowHook,
|
||||
tableHook?: SpreadsheetImportTableHook,
|
||||
): (ImportedStructuredRow & ImportedStructuredRowMetadata)[] => {
|
||||
export const addErrorsAndRunHooks = <T extends string>(
|
||||
data: (ImportedStructuredRow<T> & Partial<ImportedStructuredRowMetadata>)[],
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
rowHook?: SpreadsheetImportRowHook<T>,
|
||||
tableHook?: SpreadsheetImportTableHook<T>,
|
||||
): (ImportedStructuredRow<T> & 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<T> &
|
||||
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;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||
import lavenstein from 'js-levenshtein';
|
||||
|
||||
type AutoMatchAccumulator<T> = {
|
||||
distance: number;
|
||||
value: T;
|
||||
};
|
||||
|
||||
export const findMatch = <T extends string>(
|
||||
header: string,
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
autoMapDistance: number,
|
||||
): T | undefined => {
|
||||
const smallestValue = fields.reduce<AutoMatchAccumulator<T>>((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<T>)
|
||||
: acc;
|
||||
}, {} as AutoMatchAccumulator<T>);
|
||||
return smallestValue.distance <= autoMapDistance
|
||||
? smallestValue.value
|
||||
: undefined;
|
||||
};
|
||||
+3
-3
@@ -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 = <T extends string>(
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
columns: SpreadsheetColumns<T>,
|
||||
) =>
|
||||
fields
|
||||
.filter((field) =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||
|
||||
export const getFieldOptions = (
|
||||
fields: SpreadsheetImportFields,
|
||||
export const getFieldOptions = <T extends string>(
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
fieldKey: string,
|
||||
) => {
|
||||
const field = fields.find(({ key }) => fieldKey === key);
|
||||
|
||||
@@ -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 = <T extends string>(
|
||||
columns: SpreadsheetColumns<T>,
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
data: MatchColumnsStepProps['data'],
|
||||
autoMapDistance: number,
|
||||
) =>
|
||||
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 SpreadsheetImportField<T>;
|
||||
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];
|
||||
}
|
||||
}, []);
|
||||
+7
-7
@@ -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 = <T extends string>({
|
||||
columns,
|
||||
fields,
|
||||
data,
|
||||
}: {
|
||||
columns: SpreadsheetColumns;
|
||||
fields: SpreadsheetImportFields;
|
||||
columns: SpreadsheetColumns<T>;
|
||||
fields: SpreadsheetImportFields<T>;
|
||||
data: MatchColumnsStepProps['data'];
|
||||
}) => {
|
||||
const matchedColumns: SpreadsheetColumn[] = [];
|
||||
const matchedColumns: SpreadsheetColumn<T>[] = [];
|
||||
|
||||
const fieldsToSearch = new Fuse(fields, {
|
||||
keys: ['label'],
|
||||
@@ -30,8 +30,8 @@ export const getMatchedColumnsWithFuse = ({
|
||||
});
|
||||
|
||||
const suggestedFieldsByColumnHeader: Record<
|
||||
SpreadsheetColumn['header'],
|
||||
SpreadsheetImportField[]
|
||||
SpreadsheetColumn<T>['header'],
|
||||
SpreadsheetImportField<T>[]
|
||||
> = {};
|
||||
|
||||
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<T>,
|
||||
);
|
||||
|
||||
if (isFirstMatchValid && isFieldStillUnmatched) {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export const getShortNestedFieldLabel = (label: string) => {
|
||||
return label.split(' / ').slice(1).join(' / ');
|
||||
};
|
||||
@@ -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 = <T extends string>(
|
||||
columns: SpreadsheetColumns<T>,
|
||||
data: ImportedRow[],
|
||||
fields: SpreadsheetImportFields,
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
) =>
|
||||
data.map((row) =>
|
||||
columns.reduce((acc, column, index) => {
|
||||
@@ -101,5 +101,5 @@ export const normalizeTableData = (
|
||||
default:
|
||||
return acc;
|
||||
}
|
||||
}, {} as ImportedStructuredRow),
|
||||
}, {} as ImportedStructuredRow<T>),
|
||||
);
|
||||
|
||||
@@ -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 = <T extends string>(
|
||||
oldColumn: SpreadsheetColumn<T>,
|
||||
field?: SpreadsheetImportField<T>,
|
||||
data?: MatchColumnsStepProps['data'],
|
||||
): SpreadsheetColumn => {
|
||||
): SpreadsheetColumn<T> => {
|
||||
if (field?.fieldType.type === 'select') {
|
||||
const fieldOptions = field.fieldType.options;
|
||||
const uniqueData = uniqueEntries(
|
||||
data || [],
|
||||
oldColumn.index,
|
||||
) as SpreadsheetMatchedOptions[];
|
||||
) as SpreadsheetMatchedOptions<T>[];
|
||||
|
||||
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<T>)
|
||||
: (record as SpreadsheetMatchedOptions<T>);
|
||||
});
|
||||
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<T>)
|
||||
: ({ entry } as SpreadsheetMatchedOptions<T>);
|
||||
});
|
||||
const areAllMatched =
|
||||
matchedOptions.filter((option) => option.value).length ===
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
||||
|
||||
export const setIgnoreColumn = ({
|
||||
export const setIgnoreColumn = <T extends string>({
|
||||
header,
|
||||
index,
|
||||
}: SpreadsheetColumn): SpreadsheetColumn => ({
|
||||
}: SpreadsheetColumn<T>): SpreadsheetColumn<T> => ({
|
||||
header,
|
||||
index,
|
||||
type: SpreadsheetColumnType.ignored,
|
||||
|
||||
@@ -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 = <T>(
|
||||
oldColumn:
|
||||
| SpreadsheetMatchedSelectColumn
|
||||
| SpreadsheetMatchedSelectOptionsColumn,
|
||||
| SpreadsheetMatchedSelectColumn<T>
|
||||
| SpreadsheetMatchedSelectOptionsColumn<T>,
|
||||
entry: string,
|
||||
value: string,
|
||||
): SpreadsheetMatchedSelectColumn | SpreadsheetMatchedSelectOptionsColumn => {
|
||||
):
|
||||
| SpreadsheetMatchedSelectColumn<T>
|
||||
| SpreadsheetMatchedSelectOptionsColumn<T> => {
|
||||
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<T>[],
|
||||
type: SpreadsheetColumnType.matchedSelectOptions,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...oldColumn,
|
||||
matchedOptions: options as SpreadsheetMatchedOptions[],
|
||||
matchedOptions: options as SpreadsheetMatchedOptions<T>[],
|
||||
type: SpreadsheetColumnType.matchedSelect,
|
||||
};
|
||||
}
|
||||
|
||||
+29
@@ -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 = <T extends string>(
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
columns: SpreadsheetColumns<string>,
|
||||
) => {
|
||||
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;
|
||||
});
|
||||
};
|
||||
-42
@@ -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<SpreadsheetImportFieldOption>[] => {
|
||||
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,
|
||||
};
|
||||
},
|
||||
);
|
||||
};
|
||||
-14
@@ -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<SpreadsheetImportFieldOption>[],
|
||||
searchFilter: string,
|
||||
): readonly Readonly<SpreadsheetImportFieldOption>[] => {
|
||||
return options.filter(
|
||||
(option) =>
|
||||
option.fieldMetadataItemId === fieldMetadataItem.id &&
|
||||
option.label.toLowerCase().includes(searchFilter.toLowerCase()),
|
||||
);
|
||||
};
|
||||
-12
@@ -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)
|
||||
);
|
||||
};
|
||||
@@ -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 = <T extends string>(
|
||||
data: MatchColumnsStepProps['data'],
|
||||
index: number,
|
||||
): Partial<SpreadsheetMatchedOptions>[] =>
|
||||
): Partial<SpreadsheetMatchedOptions<T>>[] =>
|
||||
uniqBy(
|
||||
data.map((row) => ({ entry: row[index] })),
|
||||
'entry',
|
||||
|
||||
Reference in New Issue
Block a user