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:
co-authored by
Charles Bochet
parent
59c508ab7b
commit
81012ab1fb
+1
-1
@@ -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,
|
||||
|
||||
+110
-353
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+13
-2
@@ -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),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+36
-18
@@ -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);
|
||||
});
|
||||
});
|
||||
+30
-26
@@ -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 = (
|
||||
+1
@@ -146,6 +146,7 @@ export const MatchColumnsStep = ({
|
||||
columns: SpreadsheetColumns,
|
||||
) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = await matchColumnsStepHook(values, rawData, columns);
|
||||
setCurrentStepState({
|
||||
type: SpreadsheetImportStepType.validateData,
|
||||
|
||||
Reference in New Issue
Block a user