Revert "Connect - Relation on FE Importer (#13213)" (#13313)

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:
Weiko
2025-07-21 17:03:42 +02:00
committed by GitHub
parent f6aa556a16
commit 79f3fbb016
90 changed files with 1159 additions and 1572 deletions

View File

@ -1,503 +0,0 @@
import { renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useIcons } from 'twenty-ui/display';
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { IndexMetadataItem } from '@/object-metadata/types/IndexMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useBuildSpreadsheetImportFields } from '@/object-record/spreadsheet-import/hooks/useBuildSpreadSheetImportFields';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
const Wrapper = ({ children }: { children: ReactNode }) => {
return (
<RecoilRoot>
<JestObjectMetadataItemSetter>{children}</JestObjectMetadataItemSetter>
</RecoilRoot>
);
};
jest.mock('twenty-ui/display', () => ({
useIcons: jest.fn(),
}));
describe('useBuildSpreadSheetImportFields', () => {
const mockGetIcon = jest.fn().mockReturnValue('MockIcon');
const mockUseIcons = useIcons as jest.MockedFunction<typeof useIcons>;
beforeEach(() => {
mockUseIcons.mockReturnValue({
getIcon: mockGetIcon,
getIcons: () => ({}),
});
jest.clearAllMocks();
});
const createMockFieldMetadataItem = (
overrides: Partial<FieldMetadataItem> = {},
): FieldMetadataItem => ({
id: 'test-field-id',
name: 'testField',
label: 'Test Field',
type: FieldMetadataType.TEXT,
icon: 'IconTest',
isActive: true,
isCustom: false,
isSystem: false,
isNullable: true,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
...overrides,
});
const createMockObjectMetadataItem = (
overrides: Partial<ObjectMetadataItem> = {},
): ObjectMetadataItem =>
({
id: 'test-object-id',
nameSingular: 'testObject',
namePlural: 'testObjects',
labelSingular: 'Test Object',
labelPlural: 'Test Objects',
description: 'Test object description',
icon: 'IconTest',
isCustom: false,
isSystem: false,
isActive: true,
isLabelSyncedWithName: false,
isRemote: false,
isSearchable: true,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
fields: [],
...overrides,
}) as ObjectMetadataItem;
it('should build importFields for basic field types', () => {
const { result } = renderHook(
() => {
const setObjectMetadataItems = useSetRecoilState(
objectMetadataItemsState,
);
setObjectMetadataItems([]);
return useBuildSpreadsheetImportFields();
},
{ wrapper: Wrapper },
);
const fieldMetadataItems: FieldMetadataItem[] = [
createMockFieldMetadataItem({
type: FieldMetadataType.TEXT,
name: 'textField',
label: 'Text Field',
}),
createMockFieldMetadataItem({
type: FieldMetadataType.NUMBER,
name: 'numberField',
label: 'Number Field',
}),
createMockFieldMetadataItem({
type: FieldMetadataType.BOOLEAN,
name: 'booleanField',
label: 'Boolean Field',
}),
];
const spreadsheetImportFields =
result.current.buildSpreadsheetImportFields(fieldMetadataItems);
expect(spreadsheetImportFields).toHaveLength(3);
expect(spreadsheetImportFields[0]).toMatchObject({
label: 'Text Field',
key: 'textField',
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.TEXT,
isNestedField: false,
});
expect(spreadsheetImportFields[1]).toMatchObject({
label: 'Number Field',
key: 'numberField',
fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.NUMBER,
isNestedField: false,
});
expect(spreadsheetImportFields[2]).toMatchObject({
label: 'Boolean Field',
key: 'booleanField',
fieldType: { type: 'checkbox' },
fieldMetadataType: FieldMetadataType.BOOLEAN,
isNestedField: false,
});
});
it('should build importFields for select types', () => {
const { result } = renderHook(
() => {
const setObjectMetadataItems = useSetRecoilState(
objectMetadataItemsState,
);
setObjectMetadataItems([]);
return useBuildSpreadsheetImportFields();
},
{ wrapper: Wrapper },
);
const fieldMetadataItems: FieldMetadataItem[] = [
createMockFieldMetadataItem({
type: FieldMetadataType.SELECT,
name: 'selectField',
label: 'Select Field',
options: [
{
id: '1',
label: 'Option 1',
value: 'opt1',
color: 'red',
position: 0,
},
{
id: '2',
label: 'Option 2',
value: 'opt2',
color: 'blue',
position: 1,
},
],
}),
createMockFieldMetadataItem({
type: FieldMetadataType.MULTI_SELECT,
name: 'multiSelectField',
label: 'Multi Select Field',
options: [
{
id: '1',
label: 'Tag 1',
value: 'tag1',
color: 'green',
position: 0,
},
{
id: '2',
label: 'Tag 2',
value: 'tag2',
color: 'yellow',
position: 1,
},
],
}),
];
const spreadsheetImportFields =
result.current.buildSpreadsheetImportFields(fieldMetadataItems);
expect(spreadsheetImportFields).toHaveLength(2);
expect(spreadsheetImportFields[0]).toMatchObject({
label: 'Select Field',
key: 'selectField',
fieldType: {
type: 'select',
options: [
{ label: 'Option 1', value: 'opt1', color: 'red' },
{ label: 'Option 2', value: 'opt2', color: 'blue' },
],
},
fieldMetadataType: FieldMetadataType.SELECT,
});
expect(spreadsheetImportFields[1]).toMatchObject({
label: 'Multi Select Field',
key: 'multiSelectField',
fieldType: {
type: 'multiSelect',
options: [
{ label: 'Tag 1', value: 'tag1', color: 'green' },
{ label: 'Tag 2', value: 'tag2', color: 'yellow' },
],
},
fieldMetadataType: FieldMetadataType.MULTI_SELECT,
});
});
it('should build importFields for composite types (full name)', () => {
const { result } = renderHook(
() => {
const setObjectMetadataItems = useSetRecoilState(
objectMetadataItemsState,
);
setObjectMetadataItems([]);
return useBuildSpreadsheetImportFields();
},
{ wrapper: Wrapper },
);
const fieldMetadataItems: FieldMetadataItem[] = [
createMockFieldMetadataItem({
type: FieldMetadataType.FULL_NAME,
name: 'fullName',
label: 'Full Name',
}),
];
const spreadsheetImportFields =
result.current.buildSpreadsheetImportFields(fieldMetadataItems);
expect(spreadsheetImportFields.length).toBe(2);
const firstNameField = spreadsheetImportFields.find((field) =>
field.key.includes('First Name'),
);
const lastNameField = spreadsheetImportFields.find((field) =>
field.key.includes('Last Name'),
);
expect(firstNameField).toBeDefined();
expect(lastNameField).toBeDefined();
expect(firstNameField?.isNestedField).toBe(true);
expect(firstNameField?.isCompositeSubField).toBe(true);
expect(lastNameField?.isNestedField).toBe(true);
expect(lastNameField?.isCompositeSubField).toBe(true);
});
it('should filter out ACTOR fields', () => {
const { result } = renderHook(
() => {
const setObjectMetadataItems = useSetRecoilState(
objectMetadataItemsState,
);
setObjectMetadataItems([]);
return useBuildSpreadsheetImportFields();
},
{ wrapper: Wrapper },
);
const fieldMetadataItems: FieldMetadataItem[] = [
createMockFieldMetadataItem({
type: FieldMetadataType.ACTOR,
name: 'actorField',
label: 'Actor Field',
}),
createMockFieldMetadataItem({
type: FieldMetadataType.TEXT,
name: 'textField',
label: 'Text Field',
}),
];
const spreadsheetImportFields =
result.current.buildSpreadsheetImportFields(fieldMetadataItems);
expect(spreadsheetImportFields).toHaveLength(1);
expect(spreadsheetImportFields[0].fieldMetadataType).toBe(
FieldMetadataType.TEXT,
);
});
it('should return empty array for unsupported field types', () => {
const { result } = renderHook(
() => {
const setObjectMetadataItems = useSetRecoilState(
objectMetadataItemsState,
);
setObjectMetadataItems([]);
return useBuildSpreadsheetImportFields();
},
{ wrapper: Wrapper },
);
const fieldMetadataItems: FieldMetadataItem[] = [
createMockFieldMetadataItem({
type: FieldMetadataType.POSITION,
name: 'positionField',
label: 'Position Field',
}),
createMockFieldMetadataItem({
type: FieldMetadataType.TS_VECTOR,
name: 'tsVectorField',
label: 'TS Vector Field',
}),
];
const spreadsheetImportFields =
result.current.buildSpreadsheetImportFields(fieldMetadataItems);
expect(spreadsheetImportFields).toHaveLength(0);
});
it('should build importFields for relation field type', () => {
const { result } = renderHook(
() => {
const setObjectMetadataItems = useSetRecoilState(
objectMetadataItemsState,
);
const targetObjectMetadata = createMockObjectMetadataItem({
id: 'target-object-id',
nameSingular: 'company',
namePlural: 'companies',
labelSingular: 'Company',
labelPlural: 'Companies',
fields: [
createMockFieldMetadataItem({
id: 'company-id-field',
name: 'id',
label: 'ID',
type: FieldMetadataType.UUID,
}),
createMockFieldMetadataItem({
id: 'company-name-field',
name: 'name',
label: 'Name',
type: FieldMetadataType.TEXT,
}),
createMockFieldMetadataItem({
id: 'company-email-field',
name: 'emails',
label: 'Emails',
type: FieldMetadataType.EMAILS,
}),
],
indexMetadatas: [
{
id: 'primary-key-index',
name: 'primaryKeyIndex',
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
isUnique: true,
indexFieldMetadatas: [
{
id: 'index-field-1',
fieldMetadataId: 'company-id-field',
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
order: 0,
},
],
},
{
id: 'unique-name-index',
name: 'uniqueNameIndex',
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
isUnique: true,
indexFieldMetadatas: [
{
id: 'index-field-2',
fieldMetadataId: 'company-name-field',
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
order: 0,
},
],
},
{
id: 'unique-email-index',
name: 'uniqueEmailIndex',
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
isUnique: true,
indexFieldMetadatas: [
{
id: 'index-field-3',
fieldMetadataId: 'company-email-field',
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
order: 0,
},
],
},
] as IndexMetadataItem[],
});
setObjectMetadataItems([targetObjectMetadata]);
return useBuildSpreadsheetImportFields();
},
{ wrapper: Wrapper },
);
const fieldMetadataItems: FieldMetadataItem[] = [
createMockFieldMetadataItem({
type: FieldMetadataType.RELATION,
name: 'company',
label: 'Company',
relation: {
type: RelationType.MANY_TO_ONE,
targetObjectMetadata: {
id: 'target-object-id',
nameSingular: 'company',
namePlural: 'companies',
},
} as any,
}),
];
const spreadsheetImportFields =
result.current.buildSpreadsheetImportFields(fieldMetadataItems);
expect(spreadsheetImportFields).toHaveLength(4);
const idField = spreadsheetImportFields.find((field) =>
field.key.includes('id (company)'),
);
expect(idField).toBeDefined();
expect(idField).toMatchObject({
label: 'Company / ID',
key: 'id (company)',
fieldMetadataItemId: 'test-field-id',
fieldMetadataType: FieldMetadataType.RELATION,
isNestedField: true,
isRelationConnectField: true,
uniqueFieldMetadataItem: {
id: 'company-id-field',
name: 'id',
type: FieldMetadataType.UUID,
},
});
const nameField = spreadsheetImportFields.find((field) =>
field.key.includes('name (company)'),
);
expect(nameField).toBeDefined();
expect(nameField).toMatchObject({
label: 'Company / Name',
key: 'name (company)',
fieldMetadataItemId: 'test-field-id',
fieldMetadataType: FieldMetadataType.RELATION,
isNestedField: true,
isRelationConnectField: true,
uniqueFieldMetadataItem: {
id: 'company-name-field',
name: 'name',
type: FieldMetadataType.TEXT,
},
});
const primaryEmailField = spreadsheetImportFields.find((field) =>
field.key.includes('primaryEmail-emails (company)'),
);
expect(primaryEmailField).toBeDefined();
expect(primaryEmailField).toMatchObject({
isNestedField: true,
isCompositeSubField: true,
isRelationConnectField: true,
compositeSubFieldKey: 'primaryEmail',
uniqueFieldMetadataItem: {
id: 'company-email-field',
name: 'emails',
type: FieldMetadataType.EMAILS,
},
});
});
});

View File

@ -383,13 +383,9 @@ describe('useSpreadsheetCompanyImport', () => {
expect(spreadsheetImportDialogAfterOpen.options?.onSubmit).toBeInstanceOf(
Function,
);
expect(spreadsheetImportDialogAfterOpen.options).toHaveProperty(
'spreadsheetImportFields',
);
expect(spreadsheetImportDialogAfterOpen.options).toHaveProperty('fields');
expect(
Array.isArray(
spreadsheetImportDialogAfterOpen.options?.spreadsheetImportFields,
),
Array.isArray(spreadsheetImportDialogAfterOpen.options?.fields),
).toBe(true);
act(() => {

View File

@ -0,0 +1,177 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { AvailableFieldForImport } from '@/object-record/spreadsheet-import/types/AvailableFieldForImport';
import { getSpreadSheetFieldValidationDefinitions } from '@/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions';
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType';
import { useIcons } from 'twenty-ui/display';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const useBuildAvailableFieldsForImport = () => {
const { getIcon } = useIcons();
const buildAvailableFieldsForImport = (
fieldMetadataItems: FieldMetadataItem[],
) => {
const availableFieldsForImport: AvailableFieldForImport[] = [];
const createBaseField = (
fieldMetadataItem: FieldMetadataItem,
overrides: Partial<AvailableFieldForImport> = {},
customLabel?: string,
): AvailableFieldForImport => ({
Icon: getIcon(fieldMetadataItem.icon),
label: customLabel ?? fieldMetadataItem.label,
key: fieldMetadataItem.name,
fieldType: { type: 'input' },
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
customLabel ?? fieldMetadataItem.label,
),
...overrides,
});
const handleCompositeFieldWithLabels = (
fieldMetadataItem: FieldMetadataItem,
fieldType: CompositeFieldType,
) => {
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldType].subFields.forEach(
({ subFieldName, subFieldLabel, isImportable }) => {
if (!isImportable) return;
const label = `${fieldMetadataItem.label} / ${subFieldLabel}`;
availableFieldsForImport.push(
createBaseField(fieldMetadataItem, {
label,
key: `${subFieldLabel} (${fieldMetadataItem.name})`,
fieldValidationDefinitions:
getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
label,
subFieldName,
),
}),
);
},
);
};
const handleSelectField = (
fieldMetadataItem: FieldMetadataItem,
isMulti = false,
) => {
availableFieldsForImport.push(
createBaseField(fieldMetadataItem, {
fieldType: {
type: isMulti ? 'multiSelect' : 'select',
options:
fieldMetadataItem.options?.map((option) => ({
label: option.label,
value: option.value,
color: option.color,
})) || [],
},
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
`${fieldMetadataItem.label} (ID)`,
),
}),
);
};
const fieldTypeHandlers: Record<
string,
(fieldMetadataItem: FieldMetadataItem) => void
> = {
[FieldMetadataType.FULL_NAME]: (fieldMetadataItem) => {
handleCompositeFieldWithLabels(
fieldMetadataItem,
FieldMetadataType.FULL_NAME,
);
},
[FieldMetadataType.ADDRESS]: (fieldMetadataItem) => {
handleCompositeFieldWithLabels(
fieldMetadataItem,
FieldMetadataType.ADDRESS,
);
},
[FieldMetadataType.LINKS]: (fieldMetadataItem) => {
handleCompositeFieldWithLabels(
fieldMetadataItem,
FieldMetadataType.LINKS,
);
},
[FieldMetadataType.EMAILS]: (fieldMetadataItem) => {
handleCompositeFieldWithLabels(
fieldMetadataItem,
FieldMetadataType.EMAILS,
);
},
[FieldMetadataType.PHONES]: (fieldMetadataItem) => {
handleCompositeFieldWithLabels(
fieldMetadataItem,
FieldMetadataType.PHONES,
);
},
[FieldMetadataType.RICH_TEXT_V2]: (fieldMetadataItem) => {
handleCompositeFieldWithLabels(
fieldMetadataItem,
FieldMetadataType.RICH_TEXT_V2,
);
},
[FieldMetadataType.CURRENCY]: (fieldMetadataItem) => {
handleCompositeFieldWithLabels(
fieldMetadataItem,
FieldMetadataType.CURRENCY,
);
},
[FieldMetadataType.ACTOR]: (fieldMetadataItem) => {
handleCompositeFieldWithLabels(
fieldMetadataItem,
FieldMetadataType.ACTOR,
);
},
[FieldMetadataType.RELATION]: (fieldMetadataItem) => {
const label = `${fieldMetadataItem.label} (ID)`;
availableFieldsForImport.push(
createBaseField(fieldMetadataItem, {
label,
fieldValidationDefinitions:
getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
label,
),
}),
);
},
[FieldMetadataType.SELECT]: (fieldMetadataItem) => {
handleSelectField(fieldMetadataItem, false);
},
[FieldMetadataType.MULTI_SELECT]: (fieldMetadataItem) => {
handleSelectField(fieldMetadataItem, true);
},
[FieldMetadataType.BOOLEAN]: (fieldMetadataItem) => {
availableFieldsForImport.push(
createBaseField(fieldMetadataItem, {
fieldType: { type: 'checkbox' },
}),
);
},
default: (fieldMetadataItem) => {
availableFieldsForImport.push(createBaseField(fieldMetadataItem));
},
};
for (const fieldMetadataItem of fieldMetadataItems) {
const handler =
fieldTypeHandlers[fieldMetadataItem.type] || fieldTypeHandlers.default;
handler(fieldMetadataItem);
}
return availableFieldsForImport;
};
return { buildAvailableFieldsForImport };
};

View File

@ -1,284 +0,0 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { getSpreadSheetFieldValidationDefinitions } from '@/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions';
import { getRelationConnectSubFieldKey } from '@/object-record/spreadsheet-import/utils/spreadSheetGetRelationConnectSubFieldKey';
import { getCompositeSubFieldKey } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldKey';
import { getCompositeSubFieldLabelWithFieldLabel } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldLabelWithFieldLabel';
import { getRelationConnectSubFieldLabel } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetRelationConnectSubFieldLabel';
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType';
import {
SpreadsheetImportField,
SpreadsheetImportFields,
} from '@/spreadsheet-import/types';
import { useRecoilValue } from 'recoil';
import {
assertUnreachable,
getUniqueConstraintsFields,
isDefined,
} from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
export const useBuildSpreadsheetImportFields = () => {
const { getIcon } = useIcons();
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const buildSpreadsheetImportFields = (
fieldMetadataItems: FieldMetadataItem[],
): SpreadsheetImportFields => {
return fieldMetadataItems
.filter((field) => field.type !== FieldMetadataType.ACTOR)
.flatMap((fieldMetadataItem) =>
buildSpreadsheetImportField(fieldMetadataItem),
);
};
const buildSpreadsheetImportField = (
fieldMetadataItem: FieldMetadataItem,
relationConnectFieldOverrides?: Partial<SpreadsheetImportField>,
) => {
switch (fieldMetadataItem.type) {
case FieldMetadataType.ADDRESS:
case FieldMetadataType.CURRENCY:
case FieldMetadataType.EMAILS:
case FieldMetadataType.FULL_NAME:
case FieldMetadataType.LINKS:
case FieldMetadataType.PHONES:
case FieldMetadataType.RICH_TEXT_V2:
return handleCompositeFields({
fieldMetadataItem,
fieldType: fieldMetadataItem.type,
});
case FieldMetadataType.RELATION:
return handleRelationField(fieldMetadataItem);
case FieldMetadataType.SELECT:
case FieldMetadataType.MULTI_SELECT:
return [
handleSelectField(
fieldMetadataItem,
fieldMetadataItem.type === FieldMetadataType.MULTI_SELECT,
relationConnectFieldOverrides,
),
];
case FieldMetadataType.BOOLEAN:
return [
createBaseField(fieldMetadataItem, {
fieldType: { type: 'checkbox' },
...(isDefined(relationConnectFieldOverrides)
? relationConnectFieldOverrides
: {}),
}),
];
case FieldMetadataType.DATE_TIME:
case FieldMetadataType.DATE:
case FieldMetadataType.NUMBER:
case FieldMetadataType.NUMERIC:
case FieldMetadataType.TEXT:
case FieldMetadataType.UUID:
case FieldMetadataType.ARRAY:
case FieldMetadataType.RATING:
case FieldMetadataType.RAW_JSON:
return [
createBaseField(fieldMetadataItem, relationConnectFieldOverrides),
];
case FieldMetadataType.POSITION:
case FieldMetadataType.MORPH_RELATION:
case FieldMetadataType.ACTOR:
case FieldMetadataType.TS_VECTOR:
case FieldMetadataType.RICH_TEXT:
return [];
default:
return assertUnreachable(fieldMetadataItem.type);
}
};
const createBaseField = (
fieldMetadataItem: FieldMetadataItem,
overrides: Partial<SpreadsheetImportField> = {},
): SpreadsheetImportField => {
return {
Icon: getIcon(fieldMetadataItem.icon),
label: fieldMetadataItem.label,
key: fieldMetadataItem.name,
fieldMetadataItemId: fieldMetadataItem.id,
fieldType: { type: 'input' },
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
fieldMetadataItem.label,
),
isNestedField: false,
...overrides,
};
};
const handleCompositeFields = ({
fieldMetadataItem,
fieldType,
}: {
fieldMetadataItem: FieldMetadataItem;
fieldType: CompositeFieldType;
}) => {
const spreadsheetImportFields: SpreadsheetImportField[] = [];
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldType].subFields.forEach(
({ subFieldName, isImportable, subFieldLabel }) => {
if (!isImportable) return;
const label = getCompositeSubFieldLabelWithFieldLabel(
fieldMetadataItem,
subFieldLabel,
);
spreadsheetImportFields.push(
createBaseField(fieldMetadataItem, {
label,
key: getCompositeSubFieldKey(fieldMetadataItem, subFieldName),
fieldValidationDefinitions:
getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
label,
subFieldName,
),
isNestedField: true,
isCompositeSubField: true,
compositeSubFieldKey: subFieldName,
}),
);
},
);
return spreadsheetImportFields;
};
const handleCompositeFieldFromRelationConnectField = ({
fieldMetadataItem,
uniqueConstraintField,
uniqueConstraintType,
}: {
fieldMetadataItem: FieldMetadataItem;
uniqueConstraintField: FieldMetadataItem;
uniqueConstraintType: CompositeFieldType;
}) => {
const spreadsheetImportFields: SpreadsheetImportField[] = [];
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[
uniqueConstraintType
].subFields.forEach(
({ subFieldName, isImportable, isIncludedInUniqueConstraint }) => {
if (!isImportable || !isIncludedInUniqueConstraint) return;
spreadsheetImportFields.push(
createBaseField(fieldMetadataItem, {
label: getRelationConnectSubFieldLabel(
fieldMetadataItem,
uniqueConstraintField,
subFieldName,
),
key: getRelationConnectSubFieldKey(
fieldMetadataItem,
uniqueConstraintField,
subFieldName,
),
fieldValidationDefinitions:
getSpreadSheetFieldValidationDefinitions(
uniqueConstraintField.type,
uniqueConstraintField.name,
subFieldName,
),
isNestedField: true,
isCompositeSubField: true,
compositeSubFieldKey: subFieldName,
uniqueFieldMetadataItem: uniqueConstraintField,
isRelationConnectField: true,
}),
);
},
);
return spreadsheetImportFields;
};
const handleSelectField = (
fieldMetadataItem: FieldMetadataItem,
isMulti = false,
subFieldOverrides?: Record<string, any>,
) =>
createBaseField(fieldMetadataItem, {
fieldType: {
type: isMulti ? 'multiSelect' : 'select',
options:
fieldMetadataItem.options?.map((option) => ({
label: option.label,
value: option.value,
color: option.color,
})) || [],
...(isDefined(subFieldOverrides) ? subFieldOverrides : {}),
},
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
`${fieldMetadataItem.label} (ID)`,
),
});
const handleRelationField = (fieldMetadataItem: FieldMetadataItem) => {
const spreadsheetImportFields: SpreadsheetImportField[] = [];
const isManyToOneRelation =
fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE;
const targetObjectMetadataItem = objectMetadataItems?.find(
(objectMetadataItem) =>
objectMetadataItem.id ===
fieldMetadataItem.relation?.targetObjectMetadata.id,
);
if (isManyToOneRelation && isDefined(targetObjectMetadataItem)) {
const uniqueConstraintFields = getUniqueConstraintsFields<
FieldMetadataItem,
ObjectMetadataItem
>(targetObjectMetadataItem);
//todo - update logic when composite unique indexes will be supported
for (const uniqueConstraintField of uniqueConstraintFields.flat()) {
if (isCompositeFieldType(uniqueConstraintField.type)) {
spreadsheetImportFields.push(
...handleCompositeFieldFromRelationConnectField({
fieldMetadataItem,
uniqueConstraintField,
uniqueConstraintType: uniqueConstraintField.type,
}),
);
} else {
spreadsheetImportFields.push(
...buildSpreadsheetImportField(uniqueConstraintField, {
isNestedField: true,
isCompositeSubField: false,
isRelationConnectField: true,
fieldMetadataItemId: fieldMetadataItem.id,
fieldMetadataType: FieldMetadataType.RELATION,
uniqueFieldMetadataItem: uniqueConstraintField,
label: getRelationConnectSubFieldLabel(
fieldMetadataItem,
uniqueConstraintField,
),
key: getRelationConnectSubFieldKey(
fieldMetadataItem,
uniqueConstraintField,
),
}),
);
}
}
}
return spreadsheetImportFields;
};
return { buildSpreadsheetImportFields };
};

View File

@ -1,8 +1,8 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useBatchCreateManyRecords } from '@/object-record/hooks/useBatchCreateManyRecords';
import { useBuildSpreadsheetImportFields } from '@/object-record/spreadsheet-import/hooks/useBuildSpreadSheetImportFields';
import { useBuildAvailableFieldsForImport } from '@/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport';
import { buildRecordFromImportedStructuredRow } from '@/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow';
import { spreadsheetImportFilterAvailableFieldMetadataItems } from '@/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems';
import { spreadsheetImportFilterAvailableFieldMetadataItems } from '@/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts';
import { spreadsheetImportGetUnicityRowHook } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetUnicityRowHook';
import { SpreadsheetImportCreateRecordsBatchSize } from '@/spreadsheet-import/constants/SpreadsheetImportCreateRecordsBatchSize';
import { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog';
@ -10,13 +10,12 @@ import { spreadsheetImportCreatedRecordsProgressState } from '@/spreadsheet-impo
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useSetRecoilState } from 'recoil';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const useOpenObjectRecordsSpreadsheetImportDialog = (
objectNameSingular: string,
) => {
const { openSpreadsheetImportDialog } = useOpenSpreadsheetImportDialog();
const { buildSpreadsheetImportFields } = useBuildSpreadsheetImportFields();
const { openSpreadsheetImportDialog } = useOpenSpreadsheetImportDialog<any>();
const { enqueueErrorSnackBar } = useSnackBar();
const { objectMetadataItem } = useObjectMetadataItem({
@ -36,19 +35,28 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
abortController,
});
const { buildAvailableFieldsForImport } = useBuildAvailableFieldsForImport();
const openObjectRecordsSpreadsheetImportDialog = (
options?: Omit<
SpreadsheetImportDialogOptions,
SpreadsheetImportDialogOptions<any>,
'fields' | 'isOpen' | 'onClose'
>,
) => {
//All fields that can be imported (included matchable and auto-filled)
const availableFieldMetadataItemsToImport =
spreadsheetImportFilterAvailableFieldMetadataItems(
objectMetadataItem.fields,
);
const spreadsheetImportFields = buildSpreadsheetImportFields(
availableFieldMetadataItemsToImport,
const availableFieldMetadataItemsForMatching =
availableFieldMetadataItemsToImport.filter(
(fieldMetadataItem) =>
fieldMetadataItem.type !== FieldMetadataType.ACTOR,
);
const availableFieldsForMatching = buildAvailableFieldsForImport(
availableFieldMetadataItemsForMatching,
);
openSpreadsheetImportDialog({
@ -58,8 +66,7 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
const fieldMapping: Record<string, any> =
buildRecordFromImportedStructuredRow({
importedStructuredRow: record,
fieldMetadataItems: availableFieldMetadataItemsToImport,
spreadsheetImportFields,
fields: availableFieldMetadataItemsToImport,
});
return fieldMapping;
@ -76,7 +83,7 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
});
}
},
spreadsheetImportFields,
fields: availableFieldsForMatching,
availableFieldMetadataItems: availableFieldMetadataItemsToImport,
onAbortSubmit: () => {
abortController.abort();

View File

@ -0,0 +1,15 @@
import {
SpreadsheetImportFieldType,
SpreadsheetImportFieldValidationDefinition,
} from '@/spreadsheet-import/types';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { IconComponent } from 'twenty-ui/display';
export type AvailableFieldForImport = {
Icon: IconComponent;
label: string;
key: string;
fieldType: SpreadsheetImportFieldType;
fieldValidationDefinitions?: SpreadsheetImportFieldValidationDefinition[];
fieldMetadataType: FieldMetadataType;
};

View File

@ -1,20 +1,15 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation';
import { buildRecordFromImportedStructuredRow } from '@/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow';
import {
ImportedStructuredRow,
SpreadsheetImportField,
} from '@/spreadsheet-import/types';
import { ImportedStructuredRow } from '@/spreadsheet-import/types';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { RelationType } from '~/generated/graphql';
describe('buildRecordFromImportedStructuredRow', () => {
it('should successfully build a record from imported structured row', () => {
const importedStructuredRow: ImportedStructuredRow = {
const importedStructuredRow: ImportedStructuredRow<string> = {
booleanField: 'true',
numberField: '30',
multiSelectField: '["tag1", "tag2", "tag3"]',
'nameField (relationField)': 'John Doe',
relationField: 'company-123',
selectField: 'option1',
arrayField: '["item1", "item2", "item3"]',
jsonField: '{"key": "value", "nested": {"prop": "data"}}',
@ -127,9 +122,6 @@ describe('buildRecordFromImportedStructuredRow', () => {
updatedAt: '2023-01-01',
icon: 'IconBuilding',
description: null,
relation: {
type: RelationType.MANY_TO_ONE,
} as FieldMetadataItemRelation,
},
{
id: '7',
@ -345,25 +337,9 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
];
const spreadsheetImportFields = [
{
fieldMetadataItemId: '6',
isNestedField: false,
isRelationConnectField: true,
label: 'Relation Field / Name Field',
key: 'nameField (relationField)',
fieldMetadataType: FieldMetadataType.RELATION,
uniqueFieldMetadataItem: {
name: 'nameField',
type: FieldMetadataType.TEXT,
},
},
] as SpreadsheetImportField[];
const result = buildRecordFromImportedStructuredRow({
importedStructuredRow,
fieldMetadataItems: fields,
spreadsheetImportFields,
fields,
});
expect(result).toEqual({
@ -374,14 +350,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
booleanField: true,
numberField: 30,
multiSelectField: ['tag1', 'tag2', 'tag3'],
relationField: {
connect: {
where: {
nameField: 'John Doe',
},
},
},
relationFieldId: undefined,
relationFieldId: 'company-123',
selectField: 'option1',
arrayField: ['item1', 'item2', 'item3'],
jsonField: { key: 'value', nested: { prop: 'data' } },
@ -437,8 +406,8 @@ describe('buildRecordFromImportedStructuredRow', () => {
});
});
it('should successfully build a record from imported structured row with primary phone number (without calling code)', () => {
const importedStructuredRow: ImportedStructuredRow = {
it('should handle case where user provides only a primaryPhoneNumber without calling code', () => {
const importedStructuredRow: ImportedStructuredRow<string> = {
'Primary Phone Number (phoneField)': '5550123',
};
@ -461,8 +430,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
const result = buildRecordFromImportedStructuredRow({
importedStructuredRow,
fieldMetadataItems: fields,
spreadsheetImportFields: [],
fields,
});
expect(result).toEqual({
@ -472,64 +440,4 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
});
});
it('should successfully build a record from imported structured row with relation composite subfield', () => {
const importedStructuredRow: ImportedStructuredRow = {
'emailField (relationField)': 'john.doe@example.com',
};
const fields: FieldMetadataItem[] = [
{
id: '6',
name: 'relationField',
label: 'Relation Field',
type: FieldMetadataType.RELATION,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconBuilding',
description: null,
relation: {
type: RelationType.MANY_TO_ONE,
} as FieldMetadataItemRelation,
},
];
const spreadsheetImportFields = [
{
fieldMetadataItemId: '6',
isNestedField: false,
isRelationConnectField: true,
label: 'Relation Field / Email Field',
key: 'emailField (relationField)',
fieldMetadataType: FieldMetadataType.RELATION,
uniqueFieldMetadataItem: {
name: 'emailField',
type: FieldMetadataType.EMAILS,
},
compositeSubFieldKey: 'primaryEmail',
},
] as SpreadsheetImportField[];
const result = buildRecordFromImportedStructuredRow({
importedStructuredRow,
fieldMetadataItems: fields,
spreadsheetImportFields,
});
expect(result).toEqual({
relationField: {
connect: {
where: {
emailField: {
primaryEmail: 'john.doe@example.com',
},
},
},
},
});
});
});

View File

@ -79,7 +79,7 @@ describe('spreadsheetImportGetUnicityRowHook', () => {
it('should return row with error if row is not unique - index on composite field', () => {
const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem);
const testData: ImportedStructuredRow[] = [
const testData: ImportedStructuredRow<string>[] = [
{ 'Link URL (domainName)': 'https://duplicaTe.com' },
{ 'Link URL (domainName)': 'https://duplicate.com' },
{ 'Link URL (domainName)': 'https://other.com' },
@ -100,7 +100,7 @@ describe('spreadsheetImportGetUnicityRowHook', () => {
it('should return row with error if row is not unique - index on id', () => {
const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem);
const testData: ImportedStructuredRow[] = [
const testData: ImportedStructuredRow<string>[] = [
{ 'Link URL (domainName)': 'test.com', id: '1' },
{ 'Link URL (domainName)': 'test2.com', id: '1' },
{ 'Link URL (domainName)': 'test3.com', id: '3' },
@ -120,7 +120,7 @@ describe('spreadsheetImportGetUnicityRowHook', () => {
it('should return row with error if row is not unique - multi fields index', () => {
const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem);
const testData: ImportedStructuredRow[] = [
const testData: ImportedStructuredRow<string>[] = [
{ name: 'test', employees: '100', id: '1' },
{ name: 'test', employees: '100', id: '2' },
{ name: 'test', employees: '101', id: '3' },
@ -143,7 +143,7 @@ describe('spreadsheetImportGetUnicityRowHook', () => {
it('should not add error if row values are unique', () => {
const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem);
const testData: ImportedStructuredRow[] = [
const testData: ImportedStructuredRow<string>[] = [
{
name: 'test',
'Link URL (domainName)': 'test.com',

View File

@ -1,38 +1,31 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { FieldActorForInputValue } from '@/object-record/record-field/types/FieldMetadata';
import { getCompositeSubFieldKey } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldKey';
import {
ImportedStructuredRow,
SpreadsheetImportFields,
} from '@/spreadsheet-import/types';
import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey';
import { ImportedStructuredRow } from '@/spreadsheet-import/types';
import { isNonEmptyString } from '@sniptt/guards';
import { CountryCode, parsePhoneNumberWithError } from 'libphonenumber-js';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { isDefined } from 'twenty-shared/utils';
import { z } from 'zod';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { castToString } from '~/utils/castToString';
import { convertCurrencyAmountToCurrencyMicros } from '~/utils/convertCurrencyToCurrencyMicros';
import { isEmptyObject } from '~/utils/isEmptyObject';
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
type BuildRecordFromImportedStructuredRowArgs = {
importedStructuredRow: ImportedStructuredRow;
fieldMetadataItems: FieldMetadataItem[];
spreadsheetImportFields: SpreadsheetImportFields;
importedStructuredRow: ImportedStructuredRow<any>;
fields: FieldMetadataItem[];
};
const buildCompositeFieldRecord = (
field: FieldMetadataItem,
importedStructuredRow: ImportedStructuredRow,
importedStructuredRow: ImportedStructuredRow<any>,
compositeFieldConfig: Record<string, ((value: any) => any) | undefined>,
): Record<string, any> | undefined => {
const compositeFieldRecord = Object.entries(compositeFieldConfig).reduce(
(acc, [compositeFieldKey, transform]) => {
const value =
importedStructuredRow[
getCompositeSubFieldKey(field, compositeFieldKey)
];
importedStructuredRow[getSubFieldOptionKey(field, compositeFieldKey)];
return isDefined(value)
? { ...acc, [compositeFieldKey]: transform?.(value) || value }
@ -44,59 +37,9 @@ const buildCompositeFieldRecord = (
return isEmptyObject(compositeFieldRecord) ? undefined : compositeFieldRecord;
};
const buildRelationConnectFieldRecord = (
fieldMetadataItem: FieldMetadataItem,
importedStructuredRow: ImportedStructuredRow,
spreadsheetImportFields: SpreadsheetImportFields,
) => {
if (fieldMetadataItem.relation?.type !== RelationType.MANY_TO_ONE)
return undefined;
const relationConnectFields = spreadsheetImportFields.filter(
(field) =>
field.fieldMetadataItemId === fieldMetadataItem.id &&
isDefined(importedStructuredRow[field.key]) &&
isNonEmptyString(importedStructuredRow[field.key]),
);
if (relationConnectFields.length === 0) return undefined;
const relationConnectFieldValue = relationConnectFields.reduce(
(acc, field) => {
const uniqueFieldMetadataItem = field.uniqueFieldMetadataItem;
if (!isDefined(uniqueFieldMetadataItem)) return acc;
if (
isCompositeFieldType(uniqueFieldMetadataItem.type) &&
isDefined(field.compositeSubFieldKey)
) {
return {
...acc,
[uniqueFieldMetadataItem.name]: {
...(isDefined(acc?.[uniqueFieldMetadataItem.name])
? acc[uniqueFieldMetadataItem.name]
: {}),
[field.compositeSubFieldKey]: importedStructuredRow[field.key],
},
};
}
return {
...acc,
[uniqueFieldMetadataItem.name]: importedStructuredRow[field.key],
};
},
{} as Record<string, any>,
);
return isEmptyObject(relationConnectFieldValue)
? undefined
: { connect: { where: relationConnectFieldValue } };
};
export const buildRecordFromImportedStructuredRow = ({
fieldMetadataItems,
fields,
importedStructuredRow,
spreadsheetImportFields,
}: BuildRecordFromImportedStructuredRowArgs) => {
const stringArrayJSONSchema = z
.preprocess((value) => {
@ -202,7 +145,7 @@ export const buildRecordFromImportedStructuredRow = ({
},
};
for (const field of fieldMetadataItems) {
for (const field of fields) {
const importedFieldValue = importedStructuredRow[field.name];
switch (field.type) {
@ -235,12 +178,12 @@ export const buildRecordFromImportedStructuredRow = ({
const primaryPhoneNumber =
importedStructuredRow[
getCompositeSubFieldKey(field, 'primaryPhoneNumber')
getSubFieldOptionKey(field, 'primaryPhoneNumber')
];
const primaryPhoneCallingCode =
importedStructuredRow[
getCompositeSubFieldKey(field, 'primaryPhoneCallingCode')
getSubFieldOptionKey(field, 'primaryPhoneCallingCode')
];
const hasUserProvidedPrimaryPhoneNumberWithoutCallingCode =
@ -252,7 +195,7 @@ export const buildRecordFromImportedStructuredRow = ({
if (hasUserProvidedPrimaryPhoneNumberWithoutCallingCode) {
const primaryPhoneCountryCode =
importedStructuredRow[
getCompositeSubFieldKey(field, 'primaryPhoneCountryCode')
getSubFieldOptionKey(field, 'primaryPhoneCountryCode')
];
const hasUserProvidedPrimaryPhoneCountryCode =
@ -294,14 +237,22 @@ export const buildRecordFromImportedStructuredRow = ({
case FieldMetadataType.NUMERIC:
recordToBuild[field.name] = Number(importedFieldValue);
break;
case FieldMetadataType.RELATION: {
recordToBuild[field.name] = buildRelationConnectFieldRecord(
field,
importedStructuredRow,
spreadsheetImportFields,
);
case FieldMetadataType.UUID:
if (
isDefined(importedFieldValue) &&
isNonEmptyString(importedFieldValue)
) {
recordToBuild[field.name] = importedFieldValue;
}
break;
case FieldMetadataType.RELATION:
if (
isDefined(importedFieldValue) &&
isNonEmptyString(importedFieldValue)
)
recordToBuild[field.name + 'Id'] = importedFieldValue;
break;
}
case FieldMetadataType.ACTOR:
recordToBuild[field.name] = {
source: 'IMPORT',
@ -324,30 +275,11 @@ export const buildRecordFromImportedStructuredRow = ({
}
break;
}
case FieldMetadataType.UUID:
case FieldMetadataType.DATE:
case FieldMetadataType.DATE_TIME:
if (
isDefined(importedFieldValue) &&
isNonEmptyString(importedFieldValue)
) {
recordToBuild[field.name] = importedFieldValue;
}
break;
case FieldMetadataType.SELECT:
case FieldMetadataType.RATING:
case FieldMetadataType.TEXT:
default:
if (isDefined(importedFieldValue)) {
recordToBuild[field.name] = importedFieldValue;
}
break;
case FieldMetadataType.MORPH_RELATION:
case FieldMetadataType.POSITION:
case FieldMetadataType.RICH_TEXT:
case FieldMetadataType.TS_VECTOR:
break;
default:
assertUnreachable(field.type);
}
}

View File

@ -2,18 +2,20 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constants/CompositeFieldSubFieldLabel';
export const getCompositeSubFieldKey = (
export const getSubFieldOptionKey = (
fieldMetadataItem: FieldMetadataItem,
subFieldName: string,
) => {
if (!isCompositeFieldType(fieldMetadataItem.type)) {
throw new Error(
`getCompositeSubFieldKey can only be called for composite field types. Received: ${fieldMetadataItem.type}`,
`getSubFieldOptionKey can only be called for composite field types. Received: ${fieldMetadataItem.type}`,
);
}
const subFieldLabel =
COMPOSITE_FIELD_SUB_FIELD_LABELS[fieldMetadataItem.type][subFieldName];
return `${subFieldLabel} (${fieldMetadataItem.name})`;
const subFieldKey = `${subFieldLabel} (${fieldMetadataItem.name})`;
return subFieldKey;
};

View File

@ -1,10 +0,0 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isDefined } from 'twenty-shared/utils';
export const getRelationConnectSubFieldKey = (
fieldMetadataItem: FieldMetadataItem,
uniqueConstraintField: FieldMetadataItem,
compositeSubFieldKey?: string,
) => {
return `${isDefined(compositeSubFieldKey) ? `${compositeSubFieldKey}-${uniqueConstraintField.name}` : uniqueConstraintField.name} (${fieldMetadataItem.name})`;
};

View File

@ -1,8 +0,0 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
export const getCompositeSubFieldLabelWithFieldLabel = (
fieldMetadataItem: FieldMetadataItem,
subFieldLabel: string,
) => {
return `${fieldMetadataItem.label} / ${subFieldLabel}`;
};

View File

@ -1,20 +0,0 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constants/CompositeFieldSubFieldLabel';
import { isDefined } from 'twenty-shared/utils';
export const getRelationConnectSubFieldLabel = (
fieldMetadataItem: FieldMetadataItem,
uniqueFieldMetadataItem: FieldMetadataItem,
compositeSubFieldKey?: string,
) => {
const compositeSubFieldLabel =
isCompositeFieldType(fieldMetadataItem.type) &&
isDefined(compositeSubFieldKey)
? COMPOSITE_FIELD_SUB_FIELD_LABELS[fieldMetadataItem.type][
compositeSubFieldKey
]
: undefined;
return `${fieldMetadataItem.label} / ${uniqueFieldMetadataItem.label}${compositeSubFieldLabel ? ` / ${compositeSubFieldLabel}` : ''}`;
};

View File

@ -1,7 +1,6 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { getCompositeSubFieldKey } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldKey';
import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey';
import { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constants/CompositeFieldSubFieldLabel';
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
import {
@ -12,7 +11,6 @@ import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { FieldMetadataType } from 'twenty-shared/types';
import {
getUniqueConstraintsFields,
isDefined,
lowercaseUrlOriginAndRemoveTrailingSlash,
} from 'twenty-shared/utils';
@ -25,14 +23,22 @@ type Column = {
export const spreadsheetImportGetUnicityRowHook = (
objectMetadataItem: ObjectMetadataItem,
) => {
const uniqueConstraintsFields = getUniqueConstraintsFields<
FieldMetadataItem,
ObjectMetadataItem
>(objectMetadataItem);
const uniqueConstraints = objectMetadataItem.indexMetadatas.filter(
(indexMetadata) => indexMetadata.isUnique,
);
const uniqueConstraintsWithColumnNames: Column[][] = [
[{ columnName: 'id', fieldType: FieldMetadataType.UUID }],
...uniqueConstraints.map((indexMetadata) =>
indexMetadata.indexFieldMetadatas.flatMap((indexField) => {
const field = objectMetadataItem.fields.find(
(objectField) => objectField.id === indexField.fieldMetadataId,
);
if (!field) {
return [];
}
const uniqueConstraintsWithColumnNames: Column[][] =
uniqueConstraintsFields.map((uniqueConstraintFields) =>
uniqueConstraintFields.flatMap((field) => {
if (isCompositeFieldType(field.type)) {
const compositeTypeFieldConfig =
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[field.type];
@ -42,16 +48,18 @@ export const spreadsheetImportGetUnicityRowHook = (
);
return uniqueSubFields.map((subField) => ({
columnName: getCompositeSubFieldKey(field, subField.subFieldName),
columnName: getSubFieldOptionKey(field, subField.subFieldName),
fieldType: field.type,
}));
}
return [{ columnName: field.name, fieldType: field.type }];
}),
);
const rowHook: SpreadsheetImportRowHook = (row, addError, table) => {
if (uniqueConstraintsFields.length === 0) {
),
];
const rowHook: SpreadsheetImportRowHook<string> = (row, addError, table) => {
if (uniqueConstraints.length === 0) {
return row;
}
@ -87,7 +95,7 @@ export const spreadsheetImportGetUnicityRowHook = (
};
const getUniqueValues = (
row: ImportedStructuredRow,
row: ImportedStructuredRow<string>,
uniqueConstraint: Column[],
) => {
return uniqueConstraint

View File

@ -42,10 +42,18 @@ export const sanitizeRecordInput = ({
if (
isDefined(fieldMetadataItem) &&
fieldMetadataItem.type === FieldMetadataType.RELATION &&
fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE &&
!isDefined(recordInput[fieldMetadataItem.name]?.connect?.where)
fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE
) {
return undefined;
const relationIdFieldName = `${fieldMetadataItem.name}Id`;
const relationIdFieldMetadataItem = objectMetadataItem.fields.find(
(field) => field.name === relationIdFieldName,
);
const relationIdFieldValue = recordInput[relationIdFieldName];
return relationIdFieldMetadataItem
? [relationIdFieldName, relationIdFieldValue ?? null]
: undefined;
}
if (