Import - Unique value check optimization (#13761)

After an issue opened by @StephanieJoly4

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Etienne
2025-08-08 16:19:34 +02:00
committed by GitHub
co-authored by Charles Bochet
parent 59c508ab7b
commit 81012ab1fb
8 changed files with 203 additions and 406 deletions
@@ -13,9 +13,9 @@ import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRec
import { encodeCursor } from '@/apollo/utils/encodeCursor';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
import { ObjectPermissions } from 'twenty-shared/types';
/*
TODO: for now new records are added to all cached record lists, no matter what the variables (filters, orderBy, etc.) are.
@@ -16,8 +16,8 @@ export const useBatchCreateManyRecords = <
>({
objectNameSingular,
recordGqlFields,
skipPostOptimisticEffect = false,
shouldMatchRootQueryFilter,
skipPostOptimisticEffect = false,
mutationBatchSize = DEFAULT_MUTATION_BATCH_SIZE,
setBatchedRecordsCount,
abortController,
@@ -29,7 +29,7 @@ export const useBatchCreateManyRecords = <
const { createManyRecords } = useCreateManyRecords({
objectNameSingular,
recordGqlFields,
skipPostOptimisticEffect,
skipPostOptimisticEffect: skipPostOptimisticEffect,
shouldMatchRootQueryFilter,
shouldRefetchAggregateQueries: false,
});
@@ -95,6 +95,7 @@ export const useBatchCreateManyRecords = <
}
await refetchAggregateQueries();
return allCreatedRecords;
};
@@ -91,10 +91,10 @@ export const useCreateManyRecords = <
}: createManyRecordsProps) => {
const sanitizedCreateManyRecordsInput: PartialObjectRecordWithOptionalId[] =
[];
const shouldPerformOptimisticEffect = upsert !== true;
const recordOptimisticRecordsInput: PartialObjectRecordWithId[] = [];
recordsToCreate.forEach((recordToCreate) => {
const shouldDoOptimisticEffect = upsert !== true;
const idForCreation = shouldDoOptimisticEffect
const idForCreation = shouldPerformOptimisticEffect
? (recordToCreate?.id ?? v4())
: undefined;
const sanitizedRecord = {
@@ -117,7 +117,7 @@ export const useCreateManyRecords = <
sanitizedCreateManyRecordsInput.push(sanitizedRecord);
if (shouldDoOptimisticEffect) {
if (shouldPerformOptimisticEffect) {
const optimisticRecordInput = {
...computeOptimisticRecordFromInput({
cache: apolloCoreClient.cache,
@@ -186,7 +186,12 @@ export const useCreateManyRecords = <
update: (cache, { data }) => {
const records = data?.[mutationResponseField];
if (!isDefined(records?.length) || skipPostOptimisticEffect) return;
if (
!isDefined(records?.length) ||
skipPostOptimisticEffect ||
!shouldPerformOptimisticEffect
)
return;
triggerCreateRecordsOptimisticEffect({
cache,
@@ -1,5 +1,4 @@
import { gql } from '@apollo/client';
import { renderHook, waitFor } from '@testing-library/react';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { useRecoilValue } from 'recoil';
@@ -8,15 +7,37 @@ import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spread
import { useOpenObjectRecordsSpreadsheetImportDialog } from '@/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog';
import { FieldActorForInputValue } from '@/object-record/record-field/types/FieldMetadata';
import gql from 'graphql-tag';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
const mockBatchCreateManyRecords = jest.fn().mockResolvedValue([]);
jest.mock('@/object-record/hooks/useBatchCreateManyRecords', () => ({
useBatchCreateManyRecords: () => ({
batchCreateManyRecords: mockBatchCreateManyRecords,
}),
}));
const companyId = 'cb2e9f4b-20c3-4759-9315-4ffeecfaf71a';
jest.mock('uuid', () => ({
v4: jest.fn(() => companyId),
}));
const mockResult = jest.fn(() => ({
data: {
createCompanies: [
{
id: companyId,
name: 'Example Company',
employees: 0,
idealCustomerProfile: true,
__typename: 'Company',
},
],
},
}));
const companyMocks = [
{
request: {
@@ -26,309 +47,17 @@ const companyMocks = [
$upsert: Boolean
) {
createCompanies(data: $data, upsert: $upsert) {
__typename
accountOwner {
__typename
avatarUrl
colorScheme
createdAt
dateFormat
deletedAt
id
locale
name {
firstName
lastName
}
position
timeFormat
timeZone
updatedAt
userEmail
userId
}
accountOwnerId
address {
addressStreet1
addressStreet2
addressCity
addressState
addressCountry
addressPostcode
addressLat
addressLng
}
annualRecurringRevenue {
amountMicros
currencyCode
}
attachments {
edges {
node {
__typename
authorId
companyId
createdAt
deletedAt
fullPath
id
name
noteId
opportunityId
personId
petId
rocketId
surveyResultId
taskId
type
updatedAt
}
}
}
createdAt
createdBy {
source
workspaceMemberId
name
context
}
deletedAt
domainName {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
employees
favorites {
edges {
node {
__typename
companyId
createdAt
deletedAt
favoriteFolderId
forWorkspaceMemberId
id
noteId
opportunityId
personId
petId
position
rocketId
surveyResultId
taskId
updatedAt
viewId
workflowId
workflowRunId
workflowVersionId
}
}
}
id
idealCustomerProfile
introVideo {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
name
noteTargets {
edges {
node {
__typename
companyId
createdAt
deletedAt
id
noteId
opportunityId
personId
petId
rocketId
surveyResultId
updatedAt
}
}
}
opportunities {
edges {
node {
__typename
amount {
amountMicros
currencyCode
}
closeDate
companyId
createdAt
createdBy {
source
workspaceMemberId
name
context
}
deletedAt
id
name
pointOfContactId
position
stage
updatedAt
}
}
}
people {
edges {
node {
__typename
avatarUrl
city
companyId
createdAt
createdBy {
source
workspaceMemberId
name
context
}
deletedAt
emails {
primaryEmail
additionalEmails
}
id
intro
jobTitle
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
name {
firstName
lastName
}
performanceRating
phones {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
position
updatedAt
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
workPreference
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
}
}
}
position
tagline
taskTargets {
edges {
node {
__typename
companyId
createdAt
deletedAt
id
opportunityId
personId
petId
rocketId
surveyResultId
taskId
updatedAt
}
}
}
timelineActivities {
edges {
node {
__typename
companyId
createdAt
deletedAt
happensAt
id
linkedObjectMetadataId
linkedRecordCachedName
linkedRecordId
name
noteId
opportunityId
personId
petId
properties
rocketId
surveyResultId
taskId
updatedAt
workflowId
workflowRunId
workflowVersionId
workspaceMemberId
}
}
}
updatedAt
visaSponsorship
workPolicy
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
employees
idealCustomerProfile
__typename
}
}
`,
variables: {
data: [
{
createdBy: {
source: 'IMPORT',
context: {},
} satisfies FieldActorForInputValue,
employees: 0,
idealCustomerProfile: true,
name: 'Example Company',
id: companyId,
visaSponsorship: false,
deletedAt: undefined,
workPolicy: [],
},
],
upsert: true,
},
},
result: jest.fn(() => ({
data: {
createCompanies: [
{
id: companyId,
favorites: {
edges: [],
},
},
],
},
})),
variableMatcher: () => true,
result: mockResult,
},
];
@@ -342,84 +71,112 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: companyMocks,
});
// TODO: improve object metadata item seeds to have more field types to add tests on composite fields here
describe('useSpreadsheetCompanyImport', () => {
it('should work as expected', async () => {
describe('useOpenObjectRecordsSpreadsheetImportDialog', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should open dialog and configure onSubmit function correctly', async () => {
const { result } = renderHook(
() => {
const spreadsheetImportDialog = useRecoilValue(
spreadsheetImportDialogState,
);
const {
openObjectRecordsSpreadsheetImportDialog: openRecordSpreadsheetImport,
} = useOpenObjectRecordsSpreadsheetImportDialog(
CoreObjectNameSingular.Company,
);
const { openObjectRecordsSpreadsheetImportDialog } =
useOpenObjectRecordsSpreadsheetImportDialog(
CoreObjectNameSingular.Company,
);
return {
openRecordSpreadsheetImport,
openObjectRecordsSpreadsheetImportDialog,
spreadsheetImportDialog,
};
},
{
wrapper: Wrapper,
},
{ wrapper: Wrapper },
);
const { spreadsheetImportDialog, openRecordSpreadsheetImport } =
result.current;
const {
spreadsheetImportDialog,
openObjectRecordsSpreadsheetImportDialog,
} = result.current;
expect(spreadsheetImportDialog.isOpen).toBe(false);
expect(spreadsheetImportDialog.options).toBeNull();
await act(async () => {
openRecordSpreadsheetImport();
openObjectRecordsSpreadsheetImportDialog();
});
const { spreadsheetImportDialog: spreadsheetImportDialogAfterOpen } =
result.current;
const { spreadsheetImportDialog: dialogAfterOpen } = result.current;
expect(spreadsheetImportDialogAfterOpen.isOpen).toBe(true);
expect(spreadsheetImportDialogAfterOpen.options).toHaveProperty('onSubmit');
expect(spreadsheetImportDialogAfterOpen.options?.onSubmit).toBeInstanceOf(
Function,
);
expect(spreadsheetImportDialogAfterOpen.options).toHaveProperty(
'spreadsheetImportFields',
);
expect(dialogAfterOpen.isOpen).toBe(true);
expect(dialogAfterOpen.options).toHaveProperty('onSubmit');
expect(dialogAfterOpen.options?.onSubmit).toBeInstanceOf(Function);
expect(dialogAfterOpen.options).toHaveProperty('spreadsheetImportFields');
expect(
Array.isArray(
spreadsheetImportDialogAfterOpen.options?.spreadsheetImportFields,
),
Array.isArray(dialogAfterOpen.options?.spreadsheetImportFields),
).toBe(true);
});
act(() => {
spreadsheetImportDialogAfterOpen.options?.onSubmit(
it('should call batchCreateManyRecords when onSubmit is executed', async () => {
const { result } = renderHook(
() => {
const spreadsheetImportDialog = useRecoilValue(
spreadsheetImportDialogState,
);
const { openObjectRecordsSpreadsheetImportDialog } =
useOpenObjectRecordsSpreadsheetImportDialog(
CoreObjectNameSingular.Company,
);
return {
openObjectRecordsSpreadsheetImportDialog,
spreadsheetImportDialog,
};
},
{ wrapper: Wrapper },
);
await act(async () => {
result.current.openObjectRecordsSpreadsheetImportDialog();
});
const { spreadsheetImportDialog } = result.current;
const submitData = {
validStructuredRows: [
{
validStructuredRows: [
{
id: companyId,
name: 'Example Company',
idealCustomerProfile: true,
employees: '0',
},
],
invalidStructuredRows: [],
allStructuredRows: [
{
id: companyId,
name: 'Example Company',
__index: 'cbc3985f-dde9-46d1-bae2-c124141700ac',
idealCustomerProfile: true,
employees: '0',
},
],
id: companyId,
name: 'Example Company',
idealCustomerProfile: true,
employees: '0',
},
fakeCsv(),
);
],
invalidStructuredRows: [],
allStructuredRows: [
{
id: companyId,
name: 'Example Company',
__index: 'cbc3985f-dde9-46d1-bae2-c124141700ac',
idealCustomerProfile: true,
employees: '0',
},
],
};
await act(async () => {
await spreadsheetImportDialog.options?.onSubmit(submitData, fakeCsv());
});
await waitFor(() => {
expect(companyMocks[0].result).toHaveBeenCalled();
});
expect(mockBatchCreateManyRecords).toHaveBeenCalledTimes(1);
const callArgs = mockBatchCreateManyRecords.mock.calls[0][0];
expect(callArgs).toHaveProperty('recordsToCreate');
expect(callArgs).toHaveProperty('upsert', true);
expect(Array.isArray(callArgs.recordsToCreate)).toBe(true);
expect(callArgs.recordsToCreate).toHaveLength(1);
const recordToCreate = callArgs.recordsToCreate[0];
expect(recordToCreate).toHaveProperty('name', 'Example Company');
expect(recordToCreate).toHaveProperty('idealCustomerProfile', true);
expect(recordToCreate).toHaveProperty('employees', 0);
});
});
@@ -1,9 +1,11 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { generateDepthOneWithoutRelationsRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneWithoutRelationsRecordGqlFields';
import { useBatchCreateManyRecords } from '@/object-record/hooks/useBatchCreateManyRecords';
import { useBuildSpreadsheetImportFields } from '@/object-record/spreadsheet-import/hooks/useBuildSpreadSheetImportFields';
import { buildRecordFromImportedStructuredRow } from '@/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow';
import { spreadsheetImportFilterAvailableFieldMetadataItems } from '@/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems';
import { spreadsheetImportGetUnicityRowHook } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetUnicityRowHook';
import { spreadsheetImportGetUnicityTableHook } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetUnicityTableHook';
import { SpreadsheetImportCreateRecordsBatchSize } from '@/spreadsheet-import/constants/SpreadsheetImportCreateRecordsBatchSize';
import { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog';
import { spreadsheetImportCreatedRecordsProgressState } from '@/spreadsheet-import/states/spreadsheetImportCreatedRecordsProgressState';
@@ -14,6 +16,7 @@ import { useSetRecoilState } from 'recoil';
export const useOpenObjectRecordsSpreadsheetImportDialog = (
objectNameSingular: string,
) => {
const apolloCoreClient = useApolloCoreClient();
const { openSpreadsheetImportDialog } = useOpenSpreadsheetImportDialog();
const { buildSpreadsheetImportFields } = useBuildSpreadsheetImportFields();
@@ -31,6 +34,9 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
const { batchCreateManyRecords } = useBatchCreateManyRecords({
objectNameSingular,
recordGqlFields: generateDepthOneWithoutRelationsRecordGqlFields({
objectMetadataItem,
}),
mutationBatchSize: SpreadsheetImportCreateRecordsBatchSize,
setBatchedRecordsCount: setCreatedRecordsProgress,
abortController,
@@ -70,6 +76,11 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
recordsToCreate: createInputs,
upsert: true,
});
await apolloCoreClient.refetchQueries({
updateCache: (cache) => {
cache.evict({ fieldName: objectMetadataItem.namePlural });
},
});
} catch (error: any) {
enqueueErrorSnackBar({
apolloError: error,
@@ -81,7 +92,7 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
onAbortSubmit: () => {
abortController.abort();
},
rowHook: spreadsheetImportGetUnicityRowHook(objectMetadataItem),
tableHook: spreadsheetImportGetUnicityTableHook(objectMetadataItem),
});
};
@@ -1,11 +1,11 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { spreadsheetImportGetUnicityRowHook } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetUnicityRowHook';
import { spreadsheetImportGetUnicityTableHook } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetUnicityTableHook';
import { ImportedStructuredRow } from '@/spreadsheet-import/types';
import { IndexType } from '~/generated-metadata/graphql';
import { getMockCompanyObjectMetadataItem } from '~/testing/mock-data/companies';
import { getMockFieldMetadataItemOrThrow } from '~/testing/utils/getMockFieldMetadataItemOrThrow';
describe('spreadsheetImportGetUnicityRowHook', () => {
describe('spreadsheetImportGetUnicityTableHook', () => {
const baseMockCompany = getMockCompanyObjectMetadataItem();
const nameField = getMockFieldMetadataItemOrThrow({
@@ -71,7 +71,7 @@ describe('spreadsheetImportGetUnicityRowHook', () => {
};
it('should return row with error if row is not unique - index on composite field', () => {
const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem);
const hook = spreadsheetImportGetUnicityTableHook(mockObjectMetadataItem);
const testData: ImportedStructuredRow[] = [
{ 'Link URL (domainName)': 'https://duplicaTe.com' },
{ 'Link URL (domainName)': 'https://duplicate.com' },
@@ -80,18 +80,23 @@ describe('spreadsheetImportGetUnicityRowHook', () => {
const addErrorMock = jest.fn();
const result = hook(testData[1], addErrorMock, testData);
const result = hook(testData, addErrorMock);
expect(addErrorMock).toHaveBeenCalledWith('Link URL (domainName)', {
expect(addErrorMock).toHaveBeenCalledWith(0, 'Link URL (domainName)', {
message:
'This Link URL (domainName) value already exists in your import data',
level: 'error',
});
expect(result).toBe(testData[1]);
expect(addErrorMock).toHaveBeenCalledWith(1, 'Link URL (domainName)', {
message:
'This Link URL (domainName) value already exists in your import data',
level: 'error',
});
expect(result).toBe(testData);
});
it('should return row with error if row is not unique - index on id', () => {
const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem);
const hook = spreadsheetImportGetUnicityTableHook(mockObjectMetadataItem);
const testData: ImportedStructuredRow[] = [
{ 'Link URL (domainName)': 'test.com', id: '1' },
@@ -101,17 +106,21 @@ describe('spreadsheetImportGetUnicityRowHook', () => {
const addErrorMock = jest.fn();
const result = hook(testData[1], addErrorMock, testData);
const result = hook(testData, addErrorMock);
expect(addErrorMock).toHaveBeenCalledWith('id', {
expect(addErrorMock).toHaveBeenCalledWith(0, 'id', {
message: 'This id value already exists in your import data',
level: 'error',
});
expect(result).toBe(testData[1]);
expect(addErrorMock).toHaveBeenCalledWith(1, 'id', {
message: 'This id value already exists in your import data',
level: 'error',
});
expect(result).toBe(testData);
});
it('should return row with error if row is not unique - multi fields index', () => {
const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem);
const hook = spreadsheetImportGetUnicityTableHook(mockObjectMetadataItem);
const testData: ImportedStructuredRow[] = [
{ name: 'test', employees: '100', id: '1' },
@@ -121,20 +130,29 @@ describe('spreadsheetImportGetUnicityRowHook', () => {
const addErrorMock = jest.fn();
const result = hook(testData[1], addErrorMock, testData);
const result = hook(testData, addErrorMock);
expect(addErrorMock).toHaveBeenCalledWith('name', {
expect(addErrorMock).toHaveBeenCalledWith(0, 'name', {
message: 'This name value already exists in your import data',
level: 'error',
});
expect(addErrorMock).toHaveBeenCalledWith('employees', {
expect(addErrorMock).toHaveBeenCalledWith(0, 'employees', {
message: 'This employees value already exists in your import data',
level: 'error',
});
expect(result).toBe(testData[1]);
expect(addErrorMock).toHaveBeenCalledWith(1, 'name', {
message: 'This name value already exists in your import data',
level: 'error',
});
expect(addErrorMock).toHaveBeenCalledWith(1, 'employees', {
message: 'This employees value already exists in your import data',
level: 'error',
});
expect(result).toBe(testData);
});
it('should not add error if row values are unique', () => {
const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem);
const hook = spreadsheetImportGetUnicityTableHook(mockObjectMetadataItem);
const testData: ImportedStructuredRow[] = [
{
@@ -159,9 +177,9 @@ describe('spreadsheetImportGetUnicityRowHook', () => {
const addErrorMock = jest.fn();
const result = hook(testData[1], addErrorMock, testData);
const result = hook(testData, addErrorMock);
expect(addErrorMock).not.toHaveBeenCalled();
expect(result).toBe(testData[1]);
expect(result).toBe(testData);
});
});
@@ -6,9 +6,8 @@ import { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constant
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
import {
ImportedStructuredRow,
SpreadsheetImportRowHook,
SpreadsheetImportTableHook,
} from '@/spreadsheet-import/types';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { FieldMetadataType } from 'twenty-shared/types';
import {
@@ -22,7 +21,7 @@ type Column = {
fieldType: FieldMetadataType;
};
export const spreadsheetImportGetUnicityRowHook = (
export const spreadsheetImportGetUnicityTableHook = (
objectMetadataItem: ObjectMetadataItem,
) => {
const uniqueConstraintsFields = getUniqueConstraintsFields<
@@ -50,40 +49,45 @@ export const spreadsheetImportGetUnicityRowHook = (
return [{ columnName: field.name, fieldType: field.type }];
}),
);
const rowHook: SpreadsheetImportRowHook = (row, addError, table) => {
const tableHook: SpreadsheetImportTableHook = (table, addError) => {
if (uniqueConstraintsFields.length === 0) {
return row;
return table;
}
uniqueConstraintsWithColumnNames.forEach((uniqueConstraint) => {
const rowUniqueValues = getUniqueValues(row, uniqueConstraint);
for (const uniqueConstraint of uniqueConstraintsWithColumnNames) {
const uniqueValues: Record<string, number> = {};
const duplicateIndices: Set<number> = new Set();
if (!isNonEmptyString(rowUniqueValues)) {
return row;
}
table.forEach((row, index) => {
const uniqueValue = getUniqueValues(row, uniqueConstraint);
const duplicateRows = table.filter(
(r) => getUniqueValues(r, uniqueConstraint) === rowUniqueValues,
);
if (!isNonEmptyString(uniqueValue)) {
return;
}
if (duplicateRows.length <= 1) {
return row;
}
uniqueConstraint.forEach(({ columnName }) => {
if (isDefined(row[columnName])) {
addError(columnName, {
message: t`This ${columnName} value already exists in your import data`,
level: 'error',
});
if (isDefined(uniqueValues[uniqueValue])) {
const originalIndex = uniqueValues[uniqueValue];
duplicateIndices.add(originalIndex);
duplicateIndices.add(index);
} else {
uniqueValues[uniqueValue] = index;
}
});
});
return row;
duplicateIndices.forEach((duplicateIndex) => {
uniqueConstraint.forEach(({ columnName }) => {
addError(duplicateIndex, columnName, {
message: `This ${columnName} value already exists in your import data`,
level: 'error',
});
});
});
}
return table;
};
return rowHook;
return tableHook;
};
const getUniqueValues = (
@@ -146,6 +146,7 @@ export const MatchColumnsStep = ({
columns: SpreadsheetColumns,
) => {
try {
setIsLoading(true);
const data = await matchColumnsStepHook(values, rawData, columns);
setCurrentStepState({
type: SpreadsheetImportStepType.validateData,