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:
@ -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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -383,13 +383,9 @@ describe('useSpreadsheetCompanyImport', () => {
|
|||||||
expect(spreadsheetImportDialogAfterOpen.options?.onSubmit).toBeInstanceOf(
|
expect(spreadsheetImportDialogAfterOpen.options?.onSubmit).toBeInstanceOf(
|
||||||
Function,
|
Function,
|
||||||
);
|
);
|
||||||
expect(spreadsheetImportDialogAfterOpen.options).toHaveProperty(
|
expect(spreadsheetImportDialogAfterOpen.options).toHaveProperty('fields');
|
||||||
'spreadsheetImportFields',
|
|
||||||
);
|
|
||||||
expect(
|
expect(
|
||||||
Array.isArray(
|
Array.isArray(spreadsheetImportDialogAfterOpen.options?.fields),
|
||||||
spreadsheetImportDialogAfterOpen.options?.spreadsheetImportFields,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
|
|||||||
@ -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 };
|
||||||
|
};
|
||||||
@ -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 };
|
|
||||||
};
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { useBatchCreateManyRecords } from '@/object-record/hooks/useBatchCreateManyRecords';
|
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 { 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 { spreadsheetImportGetUnicityRowHook } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetUnicityRowHook';
|
||||||
import { SpreadsheetImportCreateRecordsBatchSize } from '@/spreadsheet-import/constants/SpreadsheetImportCreateRecordsBatchSize';
|
import { SpreadsheetImportCreateRecordsBatchSize } from '@/spreadsheet-import/constants/SpreadsheetImportCreateRecordsBatchSize';
|
||||||
import { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog';
|
import { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog';
|
||||||
@ -10,13 +10,12 @@ import { spreadsheetImportCreatedRecordsProgressState } from '@/spreadsheet-impo
|
|||||||
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
|
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
||||||
objectNameSingular: string,
|
objectNameSingular: string,
|
||||||
) => {
|
) => {
|
||||||
const { openSpreadsheetImportDialog } = useOpenSpreadsheetImportDialog();
|
const { openSpreadsheetImportDialog } = useOpenSpreadsheetImportDialog<any>();
|
||||||
const { buildSpreadsheetImportFields } = useBuildSpreadsheetImportFields();
|
|
||||||
|
|
||||||
const { enqueueErrorSnackBar } = useSnackBar();
|
const { enqueueErrorSnackBar } = useSnackBar();
|
||||||
|
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
@ -36,19 +35,28 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
|||||||
abortController,
|
abortController,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { buildAvailableFieldsForImport } = useBuildAvailableFieldsForImport();
|
||||||
|
|
||||||
const openObjectRecordsSpreadsheetImportDialog = (
|
const openObjectRecordsSpreadsheetImportDialog = (
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
SpreadsheetImportDialogOptions,
|
SpreadsheetImportDialogOptions<any>,
|
||||||
'fields' | 'isOpen' | 'onClose'
|
'fields' | 'isOpen' | 'onClose'
|
||||||
>,
|
>,
|
||||||
) => {
|
) => {
|
||||||
|
//All fields that can be imported (included matchable and auto-filled)
|
||||||
const availableFieldMetadataItemsToImport =
|
const availableFieldMetadataItemsToImport =
|
||||||
spreadsheetImportFilterAvailableFieldMetadataItems(
|
spreadsheetImportFilterAvailableFieldMetadataItems(
|
||||||
objectMetadataItem.fields,
|
objectMetadataItem.fields,
|
||||||
);
|
);
|
||||||
|
|
||||||
const spreadsheetImportFields = buildSpreadsheetImportFields(
|
const availableFieldMetadataItemsForMatching =
|
||||||
availableFieldMetadataItemsToImport,
|
availableFieldMetadataItemsToImport.filter(
|
||||||
|
(fieldMetadataItem) =>
|
||||||
|
fieldMetadataItem.type !== FieldMetadataType.ACTOR,
|
||||||
|
);
|
||||||
|
|
||||||
|
const availableFieldsForMatching = buildAvailableFieldsForImport(
|
||||||
|
availableFieldMetadataItemsForMatching,
|
||||||
);
|
);
|
||||||
|
|
||||||
openSpreadsheetImportDialog({
|
openSpreadsheetImportDialog({
|
||||||
@ -58,8 +66,7 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
|||||||
const fieldMapping: Record<string, any> =
|
const fieldMapping: Record<string, any> =
|
||||||
buildRecordFromImportedStructuredRow({
|
buildRecordFromImportedStructuredRow({
|
||||||
importedStructuredRow: record,
|
importedStructuredRow: record,
|
||||||
fieldMetadataItems: availableFieldMetadataItemsToImport,
|
fields: availableFieldMetadataItemsToImport,
|
||||||
spreadsheetImportFields,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return fieldMapping;
|
return fieldMapping;
|
||||||
@ -76,7 +83,7 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
spreadsheetImportFields,
|
fields: availableFieldsForMatching,
|
||||||
availableFieldMetadataItems: availableFieldMetadataItemsToImport,
|
availableFieldMetadataItems: availableFieldMetadataItemsToImport,
|
||||||
onAbortSubmit: () => {
|
onAbortSubmit: () => {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
@ -1,20 +1,15 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation';
|
|
||||||
import { buildRecordFromImportedStructuredRow } from '@/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow';
|
import { buildRecordFromImportedStructuredRow } from '@/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow';
|
||||||
import {
|
import { ImportedStructuredRow } from '@/spreadsheet-import/types';
|
||||||
ImportedStructuredRow,
|
|
||||||
SpreadsheetImportField,
|
|
||||||
} from '@/spreadsheet-import/types';
|
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { RelationType } from '~/generated/graphql';
|
|
||||||
|
|
||||||
describe('buildRecordFromImportedStructuredRow', () => {
|
describe('buildRecordFromImportedStructuredRow', () => {
|
||||||
it('should successfully build a record from imported structured row', () => {
|
it('should successfully build a record from imported structured row', () => {
|
||||||
const importedStructuredRow: ImportedStructuredRow = {
|
const importedStructuredRow: ImportedStructuredRow<string> = {
|
||||||
booleanField: 'true',
|
booleanField: 'true',
|
||||||
numberField: '30',
|
numberField: '30',
|
||||||
multiSelectField: '["tag1", "tag2", "tag3"]',
|
multiSelectField: '["tag1", "tag2", "tag3"]',
|
||||||
'nameField (relationField)': 'John Doe',
|
relationField: 'company-123',
|
||||||
selectField: 'option1',
|
selectField: 'option1',
|
||||||
arrayField: '["item1", "item2", "item3"]',
|
arrayField: '["item1", "item2", "item3"]',
|
||||||
jsonField: '{"key": "value", "nested": {"prop": "data"}}',
|
jsonField: '{"key": "value", "nested": {"prop": "data"}}',
|
||||||
@ -127,9 +122,6 @@ describe('buildRecordFromImportedStructuredRow', () => {
|
|||||||
updatedAt: '2023-01-01',
|
updatedAt: '2023-01-01',
|
||||||
icon: 'IconBuilding',
|
icon: 'IconBuilding',
|
||||||
description: null,
|
description: null,
|
||||||
relation: {
|
|
||||||
type: RelationType.MANY_TO_ONE,
|
|
||||||
} as FieldMetadataItemRelation,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '7',
|
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({
|
const result = buildRecordFromImportedStructuredRow({
|
||||||
importedStructuredRow,
|
importedStructuredRow,
|
||||||
fieldMetadataItems: fields,
|
fields,
|
||||||
spreadsheetImportFields,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
@ -374,14 +350,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
|
|||||||
booleanField: true,
|
booleanField: true,
|
||||||
numberField: 30,
|
numberField: 30,
|
||||||
multiSelectField: ['tag1', 'tag2', 'tag3'],
|
multiSelectField: ['tag1', 'tag2', 'tag3'],
|
||||||
relationField: {
|
relationFieldId: 'company-123',
|
||||||
connect: {
|
|
||||||
where: {
|
|
||||||
nameField: 'John Doe',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
relationFieldId: undefined,
|
|
||||||
selectField: 'option1',
|
selectField: 'option1',
|
||||||
arrayField: ['item1', 'item2', 'item3'],
|
arrayField: ['item1', 'item2', 'item3'],
|
||||||
jsonField: { key: 'value', nested: { prop: 'data' } },
|
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)', () => {
|
it('should handle case where user provides only a primaryPhoneNumber without calling code', () => {
|
||||||
const importedStructuredRow: ImportedStructuredRow = {
|
const importedStructuredRow: ImportedStructuredRow<string> = {
|
||||||
'Primary Phone Number (phoneField)': '5550123',
|
'Primary Phone Number (phoneField)': '5550123',
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -461,8 +430,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
|
|||||||
|
|
||||||
const result = buildRecordFromImportedStructuredRow({
|
const result = buildRecordFromImportedStructuredRow({
|
||||||
importedStructuredRow,
|
importedStructuredRow,
|
||||||
fieldMetadataItems: fields,
|
fields,
|
||||||
spreadsheetImportFields: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toEqual({
|
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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -79,7 +79,7 @@ describe('spreadsheetImportGetUnicityRowHook', () => {
|
|||||||
|
|
||||||
it('should return row with error if row is not unique - index on composite field', () => {
|
it('should return row with error if row is not unique - index on composite field', () => {
|
||||||
const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem);
|
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://duplicate.com' },
|
{ 'Link URL (domainName)': 'https://duplicate.com' },
|
||||||
{ 'Link URL (domainName)': 'https://other.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', () => {
|
it('should return row with error if row is not unique - index on id', () => {
|
||||||
const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem);
|
const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem);
|
||||||
|
|
||||||
const testData: ImportedStructuredRow[] = [
|
const testData: ImportedStructuredRow<string>[] = [
|
||||||
{ 'Link URL (domainName)': 'test.com', id: '1' },
|
{ 'Link URL (domainName)': 'test.com', id: '1' },
|
||||||
{ 'Link URL (domainName)': 'test2.com', id: '1' },
|
{ 'Link URL (domainName)': 'test2.com', id: '1' },
|
||||||
{ 'Link URL (domainName)': 'test3.com', id: '3' },
|
{ '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', () => {
|
it('should return row with error if row is not unique - multi fields index', () => {
|
||||||
const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem);
|
const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem);
|
||||||
|
|
||||||
const testData: ImportedStructuredRow[] = [
|
const testData: ImportedStructuredRow<string>[] = [
|
||||||
{ name: 'test', employees: '100', id: '1' },
|
{ name: 'test', employees: '100', id: '1' },
|
||||||
{ name: 'test', employees: '100', id: '2' },
|
{ name: 'test', employees: '100', id: '2' },
|
||||||
{ name: 'test', employees: '101', id: '3' },
|
{ name: 'test', employees: '101', id: '3' },
|
||||||
@ -143,7 +143,7 @@ describe('spreadsheetImportGetUnicityRowHook', () => {
|
|||||||
it('should not add error if row values are unique', () => {
|
it('should not add error if row values are unique', () => {
|
||||||
const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem);
|
const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem);
|
||||||
|
|
||||||
const testData: ImportedStructuredRow[] = [
|
const testData: ImportedStructuredRow<string>[] = [
|
||||||
{
|
{
|
||||||
name: 'test',
|
name: 'test',
|
||||||
'Link URL (domainName)': 'test.com',
|
'Link URL (domainName)': 'test.com',
|
||||||
|
|||||||
@ -1,38 +1,31 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
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 { FieldActorForInputValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { getCompositeSubFieldKey } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldKey';
|
import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey';
|
||||||
import {
|
import { ImportedStructuredRow } from '@/spreadsheet-import/types';
|
||||||
ImportedStructuredRow,
|
|
||||||
SpreadsheetImportFields,
|
|
||||||
} from '@/spreadsheet-import/types';
|
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { CountryCode, parsePhoneNumberWithError } from 'libphonenumber-js';
|
import { CountryCode, parsePhoneNumberWithError } from 'libphonenumber-js';
|
||||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { castToString } from '~/utils/castToString';
|
import { castToString } from '~/utils/castToString';
|
||||||
import { convertCurrencyAmountToCurrencyMicros } from '~/utils/convertCurrencyToCurrencyMicros';
|
import { convertCurrencyAmountToCurrencyMicros } from '~/utils/convertCurrencyToCurrencyMicros';
|
||||||
import { isEmptyObject } from '~/utils/isEmptyObject';
|
import { isEmptyObject } from '~/utils/isEmptyObject';
|
||||||
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
|
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
|
||||||
|
|
||||||
type BuildRecordFromImportedStructuredRowArgs = {
|
type BuildRecordFromImportedStructuredRowArgs = {
|
||||||
importedStructuredRow: ImportedStructuredRow;
|
importedStructuredRow: ImportedStructuredRow<any>;
|
||||||
fieldMetadataItems: FieldMetadataItem[];
|
fields: FieldMetadataItem[];
|
||||||
spreadsheetImportFields: SpreadsheetImportFields;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildCompositeFieldRecord = (
|
const buildCompositeFieldRecord = (
|
||||||
field: FieldMetadataItem,
|
field: FieldMetadataItem,
|
||||||
importedStructuredRow: ImportedStructuredRow,
|
importedStructuredRow: ImportedStructuredRow<any>,
|
||||||
compositeFieldConfig: Record<string, ((value: any) => any) | undefined>,
|
compositeFieldConfig: Record<string, ((value: any) => any) | undefined>,
|
||||||
): Record<string, any> | undefined => {
|
): Record<string, any> | undefined => {
|
||||||
const compositeFieldRecord = Object.entries(compositeFieldConfig).reduce(
|
const compositeFieldRecord = Object.entries(compositeFieldConfig).reduce(
|
||||||
(acc, [compositeFieldKey, transform]) => {
|
(acc, [compositeFieldKey, transform]) => {
|
||||||
const value =
|
const value =
|
||||||
importedStructuredRow[
|
importedStructuredRow[getSubFieldOptionKey(field, compositeFieldKey)];
|
||||||
getCompositeSubFieldKey(field, compositeFieldKey)
|
|
||||||
];
|
|
||||||
|
|
||||||
return isDefined(value)
|
return isDefined(value)
|
||||||
? { ...acc, [compositeFieldKey]: transform?.(value) || value }
|
? { ...acc, [compositeFieldKey]: transform?.(value) || value }
|
||||||
@ -44,59 +37,9 @@ const buildCompositeFieldRecord = (
|
|||||||
return isEmptyObject(compositeFieldRecord) ? undefined : compositeFieldRecord;
|
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 = ({
|
export const buildRecordFromImportedStructuredRow = ({
|
||||||
fieldMetadataItems,
|
fields,
|
||||||
importedStructuredRow,
|
importedStructuredRow,
|
||||||
spreadsheetImportFields,
|
|
||||||
}: BuildRecordFromImportedStructuredRowArgs) => {
|
}: BuildRecordFromImportedStructuredRowArgs) => {
|
||||||
const stringArrayJSONSchema = z
|
const stringArrayJSONSchema = z
|
||||||
.preprocess((value) => {
|
.preprocess((value) => {
|
||||||
@ -202,7 +145,7 @@ export const buildRecordFromImportedStructuredRow = ({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const field of fieldMetadataItems) {
|
for (const field of fields) {
|
||||||
const importedFieldValue = importedStructuredRow[field.name];
|
const importedFieldValue = importedStructuredRow[field.name];
|
||||||
|
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
@ -235,12 +178,12 @@ export const buildRecordFromImportedStructuredRow = ({
|
|||||||
|
|
||||||
const primaryPhoneNumber =
|
const primaryPhoneNumber =
|
||||||
importedStructuredRow[
|
importedStructuredRow[
|
||||||
getCompositeSubFieldKey(field, 'primaryPhoneNumber')
|
getSubFieldOptionKey(field, 'primaryPhoneNumber')
|
||||||
];
|
];
|
||||||
|
|
||||||
const primaryPhoneCallingCode =
|
const primaryPhoneCallingCode =
|
||||||
importedStructuredRow[
|
importedStructuredRow[
|
||||||
getCompositeSubFieldKey(field, 'primaryPhoneCallingCode')
|
getSubFieldOptionKey(field, 'primaryPhoneCallingCode')
|
||||||
];
|
];
|
||||||
|
|
||||||
const hasUserProvidedPrimaryPhoneNumberWithoutCallingCode =
|
const hasUserProvidedPrimaryPhoneNumberWithoutCallingCode =
|
||||||
@ -252,7 +195,7 @@ export const buildRecordFromImportedStructuredRow = ({
|
|||||||
if (hasUserProvidedPrimaryPhoneNumberWithoutCallingCode) {
|
if (hasUserProvidedPrimaryPhoneNumberWithoutCallingCode) {
|
||||||
const primaryPhoneCountryCode =
|
const primaryPhoneCountryCode =
|
||||||
importedStructuredRow[
|
importedStructuredRow[
|
||||||
getCompositeSubFieldKey(field, 'primaryPhoneCountryCode')
|
getSubFieldOptionKey(field, 'primaryPhoneCountryCode')
|
||||||
];
|
];
|
||||||
|
|
||||||
const hasUserProvidedPrimaryPhoneCountryCode =
|
const hasUserProvidedPrimaryPhoneCountryCode =
|
||||||
@ -294,14 +237,22 @@ export const buildRecordFromImportedStructuredRow = ({
|
|||||||
case FieldMetadataType.NUMERIC:
|
case FieldMetadataType.NUMERIC:
|
||||||
recordToBuild[field.name] = Number(importedFieldValue);
|
recordToBuild[field.name] = Number(importedFieldValue);
|
||||||
break;
|
break;
|
||||||
case FieldMetadataType.RELATION: {
|
case FieldMetadataType.UUID:
|
||||||
recordToBuild[field.name] = buildRelationConnectFieldRecord(
|
if (
|
||||||
field,
|
isDefined(importedFieldValue) &&
|
||||||
importedStructuredRow,
|
isNonEmptyString(importedFieldValue)
|
||||||
spreadsheetImportFields,
|
) {
|
||||||
);
|
recordToBuild[field.name] = importedFieldValue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case FieldMetadataType.RELATION:
|
||||||
|
if (
|
||||||
|
isDefined(importedFieldValue) &&
|
||||||
|
isNonEmptyString(importedFieldValue)
|
||||||
|
)
|
||||||
|
recordToBuild[field.name + 'Id'] = importedFieldValue;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
case FieldMetadataType.ACTOR:
|
case FieldMetadataType.ACTOR:
|
||||||
recordToBuild[field.name] = {
|
recordToBuild[field.name] = {
|
||||||
source: 'IMPORT',
|
source: 'IMPORT',
|
||||||
@ -324,30 +275,11 @@ export const buildRecordFromImportedStructuredRow = ({
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case FieldMetadataType.UUID:
|
default:
|
||||||
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:
|
|
||||||
if (isDefined(importedFieldValue)) {
|
if (isDefined(importedFieldValue)) {
|
||||||
recordToBuild[field.name] = importedFieldValue;
|
recordToBuild[field.name] = importedFieldValue;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case FieldMetadataType.MORPH_RELATION:
|
|
||||||
case FieldMetadataType.POSITION:
|
|
||||||
case FieldMetadataType.RICH_TEXT:
|
|
||||||
case FieldMetadataType.TS_VECTOR:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
assertUnreachable(field.type);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,18 +2,20 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
|||||||
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
||||||
import { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constants/CompositeFieldSubFieldLabel';
|
import { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constants/CompositeFieldSubFieldLabel';
|
||||||
|
|
||||||
export const getCompositeSubFieldKey = (
|
export const getSubFieldOptionKey = (
|
||||||
fieldMetadataItem: FieldMetadataItem,
|
fieldMetadataItem: FieldMetadataItem,
|
||||||
subFieldName: string,
|
subFieldName: string,
|
||||||
) => {
|
) => {
|
||||||
if (!isCompositeFieldType(fieldMetadataItem.type)) {
|
if (!isCompositeFieldType(fieldMetadataItem.type)) {
|
||||||
throw new Error(
|
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 =
|
const subFieldLabel =
|
||||||
COMPOSITE_FIELD_SUB_FIELD_LABELS[fieldMetadataItem.type][subFieldName];
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[fieldMetadataItem.type][subFieldName];
|
||||||
|
|
||||||
return `${subFieldLabel} (${fieldMetadataItem.name})`;
|
const subFieldKey = `${subFieldLabel} (${fieldMetadataItem.name})`;
|
||||||
|
|
||||||
|
return subFieldKey;
|
||||||
};
|
};
|
||||||
@ -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})`;
|
|
||||||
};
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
|
||||||
|
|
||||||
export const getCompositeSubFieldLabelWithFieldLabel = (
|
|
||||||
fieldMetadataItem: FieldMetadataItem,
|
|
||||||
subFieldLabel: string,
|
|
||||||
) => {
|
|
||||||
return `${fieldMetadataItem.label} / ${subFieldLabel}`;
|
|
||||||
};
|
|
||||||
@ -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}` : ''}`;
|
|
||||||
};
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
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 { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constants/CompositeFieldSubFieldLabel';
|
||||||
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
||||||
import {
|
import {
|
||||||
@ -12,7 +11,6 @@ import { t } from '@lingui/core/macro';
|
|||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
import {
|
import {
|
||||||
getUniqueConstraintsFields,
|
|
||||||
isDefined,
|
isDefined,
|
||||||
lowercaseUrlOriginAndRemoveTrailingSlash,
|
lowercaseUrlOriginAndRemoveTrailingSlash,
|
||||||
} from 'twenty-shared/utils';
|
} from 'twenty-shared/utils';
|
||||||
@ -25,14 +23,22 @@ type Column = {
|
|||||||
export const spreadsheetImportGetUnicityRowHook = (
|
export const spreadsheetImportGetUnicityRowHook = (
|
||||||
objectMetadataItem: ObjectMetadataItem,
|
objectMetadataItem: ObjectMetadataItem,
|
||||||
) => {
|
) => {
|
||||||
const uniqueConstraintsFields = getUniqueConstraintsFields<
|
const uniqueConstraints = objectMetadataItem.indexMetadatas.filter(
|
||||||
FieldMetadataItem,
|
(indexMetadata) => indexMetadata.isUnique,
|
||||||
ObjectMetadataItem
|
);
|
||||||
>(objectMetadataItem);
|
|
||||||
|
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)) {
|
if (isCompositeFieldType(field.type)) {
|
||||||
const compositeTypeFieldConfig =
|
const compositeTypeFieldConfig =
|
||||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[field.type];
|
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[field.type];
|
||||||
@ -42,16 +48,18 @@ export const spreadsheetImportGetUnicityRowHook = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
return uniqueSubFields.map((subField) => ({
|
return uniqueSubFields.map((subField) => ({
|
||||||
columnName: getCompositeSubFieldKey(field, subField.subFieldName),
|
columnName: getSubFieldOptionKey(field, subField.subFieldName),
|
||||||
fieldType: field.type,
|
fieldType: field.type,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return [{ columnName: field.name, 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;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +95,7 @@ export const spreadsheetImportGetUnicityRowHook = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getUniqueValues = (
|
const getUniqueValues = (
|
||||||
row: ImportedStructuredRow,
|
row: ImportedStructuredRow<string>,
|
||||||
uniqueConstraint: Column[],
|
uniqueConstraint: Column[],
|
||||||
) => {
|
) => {
|
||||||
return uniqueConstraint
|
return uniqueConstraint
|
||||||
|
|||||||
@ -42,10 +42,18 @@ export const sanitizeRecordInput = ({
|
|||||||
if (
|
if (
|
||||||
isDefined(fieldMetadataItem) &&
|
isDefined(fieldMetadataItem) &&
|
||||||
fieldMetadataItem.type === FieldMetadataType.RELATION &&
|
fieldMetadataItem.type === FieldMetadataType.RELATION &&
|
||||||
fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE &&
|
fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE
|
||||||
!isDefined(recordInput[fieldMetadataItem.name]?.connect?.where)
|
|
||||||
) {
|
) {
|
||||||
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 (
|
if (
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|||||||
|
|
||||||
//TODO : isIncludedInUniqueConstraint refactor - https://github.com/twentyhq/core-team-issues/issues/1097
|
//TODO : isIncludedInUniqueConstraint refactor - https://github.com/twentyhq/core-team-issues/issues/1097
|
||||||
|
|
||||||
export type CompositeSubFieldConfig<T> = {
|
type CompositeSubFieldConfig<T> = {
|
||||||
subFieldName: keyof T;
|
subFieldName: keyof T;
|
||||||
subFieldLabel: string;
|
subFieldLabel: string;
|
||||||
isImportable: boolean;
|
isImportable: boolean;
|
||||||
@ -258,7 +258,7 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
|
|||||||
.firstName,
|
.firstName,
|
||||||
isImportable: true,
|
isImportable: true,
|
||||||
isFilterable: true,
|
isFilterable: true,
|
||||||
isIncludedInUniqueConstraint: false,
|
isIncludedInUniqueConstraint: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
subFieldName: 'lastName',
|
subFieldName: 'lastName',
|
||||||
@ -267,7 +267,7 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
|
|||||||
.lastName,
|
.lastName,
|
||||||
isImportable: true,
|
isImportable: true,
|
||||||
isFilterable: true,
|
isFilterable: true,
|
||||||
isIncludedInUniqueConstraint: false,
|
isIncludedInUniqueConstraint: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
exampleValues: [
|
exampleValues: [
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { defaultSpreadsheetImportProps } from '@/spreadsheet-import/provider/components/SpreadsheetImport';
|
import { defaultSpreadsheetImportProps } from '@/spreadsheet-import/provider/components/SpreadsheetImport';
|
||||||
import {
|
import {
|
||||||
SpreadsheetImportDialogOptions,
|
SpreadsheetImportDialogOptions,
|
||||||
SpreadsheetImportFields
|
SpreadsheetImportFields
|
||||||
} from '@/spreadsheet-import/types';
|
} from '@/spreadsheet-import/types';
|
||||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
@ -16,6 +16,7 @@ const fields = [
|
|||||||
fieldType: {
|
fieldType: {
|
||||||
type: 'input',
|
type: 'input',
|
||||||
},
|
},
|
||||||
|
example: 'Stephanie',
|
||||||
fieldValidationDefinitions: [
|
fieldValidationDefinitions: [
|
||||||
{
|
{
|
||||||
rule: 'required',
|
rule: 'required',
|
||||||
@ -23,8 +24,6 @@ const fields = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
fieldMetadataType: FieldMetadataType.TEXT,
|
fieldMetadataType: FieldMetadataType.TEXT,
|
||||||
fieldMetadataItemId: '1',
|
|
||||||
isNestedField: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Icon: null,
|
Icon: null,
|
||||||
@ -34,6 +33,7 @@ const fields = [
|
|||||||
fieldType: {
|
fieldType: {
|
||||||
type: 'input',
|
type: 'input',
|
||||||
},
|
},
|
||||||
|
example: 'McDonald',
|
||||||
fieldValidationDefinitions: [
|
fieldValidationDefinitions: [
|
||||||
{
|
{
|
||||||
rule: 'unique',
|
rule: 'unique',
|
||||||
@ -42,9 +42,6 @@ const fields = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: 'Family / Last name',
|
description: 'Family / Last name',
|
||||||
fieldMetadataType: FieldMetadataType.TEXT,
|
|
||||||
fieldMetadataItemId: '2',
|
|
||||||
isNestedField: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Icon: null,
|
Icon: null,
|
||||||
@ -54,6 +51,7 @@ const fields = [
|
|||||||
fieldType: {
|
fieldType: {
|
||||||
type: 'input',
|
type: 'input',
|
||||||
},
|
},
|
||||||
|
example: '23',
|
||||||
fieldValidationDefinitions: [
|
fieldValidationDefinitions: [
|
||||||
{
|
{
|
||||||
rule: 'regex',
|
rule: 'regex',
|
||||||
@ -62,14 +60,12 @@ const fields = [
|
|||||||
level: 'warning',
|
level: 'warning',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
fieldMetadataType: FieldMetadataType.TEXT,
|
|
||||||
fieldMetadataItemId: '3',
|
|
||||||
isNestedField: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Icon: null,
|
Icon: null,
|
||||||
label: 'Team',
|
label: 'Team',
|
||||||
key: 'team',
|
key: 'team',
|
||||||
|
alternateMatches: ['department'],
|
||||||
fieldType: {
|
fieldType: {
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
@ -77,31 +73,28 @@ const fields = [
|
|||||||
{ label: 'Team Two', value: 'two' },
|
{ label: 'Team Two', value: 'two' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
example: 'Team one',
|
||||||
fieldValidationDefinitions: [
|
fieldValidationDefinitions: [
|
||||||
{
|
{
|
||||||
rule: 'required',
|
rule: 'required',
|
||||||
errorMessage: 'Team is required',
|
errorMessage: 'Team is required',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
fieldMetadataType: FieldMetadataType.TEXT,
|
|
||||||
fieldMetadataItemId: '4',
|
|
||||||
isNestedField: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Icon: null,
|
Icon: null,
|
||||||
label: 'Is manager',
|
label: 'Is manager',
|
||||||
key: 'is_manager',
|
key: 'is_manager',
|
||||||
|
alternateMatches: ['manages'],
|
||||||
fieldType: {
|
fieldType: {
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
booleanMatches: {},
|
booleanMatches: {},
|
||||||
},
|
},
|
||||||
fieldMetadataType: FieldMetadataType.TEXT,
|
example: 'true',
|
||||||
fieldMetadataItemId: '5',
|
|
||||||
isNestedField: false,
|
|
||||||
},
|
},
|
||||||
] as SpreadsheetImportFields;
|
] as SpreadsheetImportFields<string>;
|
||||||
|
|
||||||
export const importedColums: SpreadsheetColumns = [
|
export const importedColums: SpreadsheetColumns<string> = [
|
||||||
{
|
{
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
index: 0,
|
index: 0,
|
||||||
@ -128,13 +121,13 @@ export const importedColums: SpreadsheetColumns = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const mockComponentBehaviourForTypes = (
|
const mockComponentBehaviourForTypes = <T extends string>(
|
||||||
props: SpreadsheetImportDialogOptions,
|
props: SpreadsheetImportDialogOptions<T>,
|
||||||
) => props;
|
) => props;
|
||||||
|
|
||||||
export const mockRsiValues = mockComponentBehaviourForTypes({
|
export const mockRsiValues = mockComponentBehaviourForTypes({
|
||||||
...defaultSpreadsheetImportProps,
|
...defaultSpreadsheetImportProps,
|
||||||
spreadsheetImportFields: fields,
|
fields: fields,
|
||||||
onSubmit: async () => {
|
onSubmit: async () => {
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { getFieldMetadataTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFieldMetadataTypeLabel';
|
import { getFieldMetadataTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFieldMetadataTypeLabel';
|
||||||
|
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
||||||
import { DO_NOT_IMPORT_OPTION_KEY } from '@/spreadsheet-import/constants/DoNotImportOptionKey';
|
import { DO_NOT_IMPORT_OPTION_KEY } from '@/spreadsheet-import/constants/DoNotImportOptionKey';
|
||||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||||
import { hasNestedFields } from '@/spreadsheet-import/utils/spreadsheetImportHasNestedFields';
|
|
||||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||||
@ -139,7 +139,7 @@ export const MatchColumnSelectFieldSelectDropdownContent = ({
|
|||||||
LeftIcon={getIcon(field.icon)}
|
LeftIcon={getIcon(field.icon)}
|
||||||
text={field.label}
|
text={field.label}
|
||||||
contextualText={getFieldMetadataTypeLabel(field.type)}
|
contextualText={getFieldMetadataTypeLabel(field.type)}
|
||||||
hasSubMenu={hasNestedFields(field)}
|
hasSubMenu={isCompositeFieldType(field.type)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { SpreadsheetImportFieldOption } from '@/spreadsheet-import/types/SpreadsheetImportFieldOption';
|
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
|
||||||
import { getSubFieldOptions } from '@/spreadsheet-import/utils/spreadsheetImportGetSubFieldOptions';
|
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
||||||
import { hasNestedFields } from '@/spreadsheet-import/utils/spreadsheetImportHasNestedFields';
|
import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey';
|
||||||
|
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
||||||
|
import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType';
|
||||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||||
@ -10,8 +12,15 @@ import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/Dropdow
|
|||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { IconChevronLeft, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import {
|
||||||
|
IconChevronLeft,
|
||||||
|
OverflowingTextWithTooltip,
|
||||||
|
useIcons,
|
||||||
|
} from 'twenty-ui/display';
|
||||||
|
import { SelectOption } from 'twenty-ui/input';
|
||||||
import { MenuItem } from 'twenty-ui/navigation';
|
import { MenuItem } from 'twenty-ui/navigation';
|
||||||
|
import { ReadonlyDeep } from 'type-fest';
|
||||||
|
|
||||||
export const MatchColumnSelectSubFieldSelectDropdownContent = ({
|
export const MatchColumnSelectSubFieldSelectDropdownContent = ({
|
||||||
fieldMetadataItem,
|
fieldMetadataItem,
|
||||||
@ -21,11 +30,13 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({
|
|||||||
}: {
|
}: {
|
||||||
fieldMetadataItem: FieldMetadataItem;
|
fieldMetadataItem: FieldMetadataItem;
|
||||||
onSubFieldSelect: (subFieldNameSelected: string) => void;
|
onSubFieldSelect: (subFieldNameSelected: string) => void;
|
||||||
options: readonly Readonly<SpreadsheetImportFieldOption>[];
|
options: readonly ReadonlyDeep<SelectOption>[];
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [searchFilter, setSearchFilter] = useState('');
|
const [searchFilter, setSearchFilter] = useState('');
|
||||||
|
|
||||||
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = event.currentTarget.value;
|
const value = event.currentTarget.value;
|
||||||
|
|
||||||
@ -41,15 +52,31 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({
|
|||||||
onBack();
|
onBack();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!hasNestedFields(fieldMetadataItem)) {
|
if (!isCompositeFieldType(fieldMetadataItem.type)) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const subFieldOptions = getSubFieldOptions(
|
const fieldMetadataItemSettings =
|
||||||
fieldMetadataItem,
|
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldMetadataItem.type];
|
||||||
options,
|
|
||||||
searchFilter,
|
const subFieldsThatExistInOptions = fieldMetadataItemSettings.subFields
|
||||||
);
|
.filter(({ subFieldName }) => {
|
||||||
|
const optionKey = getSubFieldOptionKey(fieldMetadataItem, subFieldName);
|
||||||
|
|
||||||
|
const correspondingOption = options.find(
|
||||||
|
(option) => option.value === optionKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
return isDefined(correspondingOption);
|
||||||
|
})
|
||||||
|
.filter(({ subFieldName }) =>
|
||||||
|
getCompositeSubFieldLabel(
|
||||||
|
fieldMetadataItem.type as CompositeFieldType,
|
||||||
|
subFieldName,
|
||||||
|
)
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchFilter.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
|
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
|
||||||
@ -70,17 +97,24 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({
|
|||||||
/>
|
/>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
{subFieldOptions.map(
|
{subFieldsThatExistInOptions.map(({ subFieldName }) => (
|
||||||
({ value, shortLabelForNestedField, Icon, disabled }) => (
|
<MenuItem
|
||||||
<MenuItem
|
key={subFieldName}
|
||||||
key={value}
|
onClick={() => handleSubFieldSelect(subFieldName)}
|
||||||
onClick={() => handleSubFieldSelect(value)}
|
LeftIcon={getIcon(fieldMetadataItem.icon)}
|
||||||
LeftIcon={Icon}
|
text={getCompositeSubFieldLabel(
|
||||||
text={shortLabelForNestedField}
|
fieldMetadataItem.type as CompositeFieldType,
|
||||||
disabled={disabled}
|
subFieldName,
|
||||||
/>
|
)}
|
||||||
),
|
disabled={
|
||||||
)}
|
options.find(
|
||||||
|
(option) =>
|
||||||
|
option.value ===
|
||||||
|
getSubFieldOptionKey(fieldMetadataItem, subFieldName),
|
||||||
|
)?.disabled
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
</DropdownContent>
|
</DropdownContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,11 +3,10 @@ import { ReadonlyDeep } from 'type-fest';
|
|||||||
|
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
||||||
|
import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey';
|
||||||
import { MatchColumnSelectFieldSelectDropdownContent } from '@/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent';
|
import { MatchColumnSelectFieldSelectDropdownContent } from '@/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent';
|
||||||
import { MatchColumnSelectSubFieldSelectDropdownContent } from '@/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent';
|
import { MatchColumnSelectSubFieldSelectDropdownContent } from '@/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent';
|
||||||
import { DO_NOT_IMPORT_OPTION_KEY } from '@/spreadsheet-import/constants/DoNotImportOptionKey';
|
import { DO_NOT_IMPORT_OPTION_KEY } from '@/spreadsheet-import/constants/DoNotImportOptionKey';
|
||||||
import { SpreadsheetImportFieldOption } from '@/spreadsheet-import/types/SpreadsheetImportFieldOption';
|
|
||||||
import { hasNestedFields } from '@/spreadsheet-import/utils/spreadsheetImportHasNestedFields';
|
|
||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
@ -20,7 +19,7 @@ interface MatchColumnToFieldSelectProps {
|
|||||||
columnIndex: string;
|
columnIndex: string;
|
||||||
onChange: (value: ReadonlyDeep<SelectOption> | null) => void;
|
onChange: (value: ReadonlyDeep<SelectOption> | null) => void;
|
||||||
value?: ReadonlyDeep<SelectOption>;
|
value?: ReadonlyDeep<SelectOption>;
|
||||||
options: readonly Readonly<SpreadsheetImportFieldOption>[];
|
options: readonly ReadonlyDeep<SelectOption>[];
|
||||||
suggestedOptions: readonly ReadonlyDeep<SelectOption>[];
|
suggestedOptions: readonly ReadonlyDeep<SelectOption>[];
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
@ -71,7 +70,12 @@ export const MatchColumnToFieldSelect = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const correspondingOption = options.find((option) => {
|
const correspondingOption = options.find((option) => {
|
||||||
return option.value === subFieldNameSelected;
|
const optionKey = getSubFieldOptionKey(
|
||||||
|
selectedFieldMetadataItem,
|
||||||
|
subFieldNameSelected,
|
||||||
|
);
|
||||||
|
|
||||||
|
return option.value === optionKey;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isDefined(correspondingOption)) {
|
if (isDefined(correspondingOption)) {
|
||||||
@ -108,9 +112,9 @@ export const MatchColumnToFieldSelect = ({
|
|||||||
closeDropdown(dropdownId);
|
closeDropdown(dropdownId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldShowNestedField =
|
const shouldShowSubField =
|
||||||
isDefined(selectedFieldMetadataItem) &&
|
isDefined(selectedFieldMetadataItem) &&
|
||||||
hasNestedFields(selectedFieldMetadataItem);
|
isCompositeFieldType(selectedFieldMetadataItem.type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@ -125,7 +129,7 @@ export const MatchColumnToFieldSelect = ({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
dropdownComponents={
|
dropdownComponents={
|
||||||
shouldShowNestedField ? (
|
shouldShowSubField ? (
|
||||||
<MatchColumnSelectSubFieldSelectDropdownContent
|
<MatchColumnSelectSubFieldSelectDropdownContent
|
||||||
fieldMetadataItem={selectedFieldMetadataItem}
|
fieldMetadataItem={selectedFieldMetadataItem}
|
||||||
onSubFieldSelect={handleSubFieldSelect}
|
onSubFieldSelect={handleSubFieldSelect}
|
||||||
|
|||||||
@ -5,16 +5,16 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
|||||||
|
|
||||||
export const RsiContext = createContext({} as any);
|
export const RsiContext = createContext({} as any);
|
||||||
|
|
||||||
type ReactSpreadsheetImportContextProviderProps = {
|
type ReactSpreadsheetImportContextProviderProps<T extends string> = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
values: SpreadsheetImportDialogOptions;
|
values: SpreadsheetImportDialogOptions<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReactSpreadsheetImportContextProvider = ({
|
export const ReactSpreadsheetImportContextProvider = <T extends string>({
|
||||||
children,
|
children,
|
||||||
values,
|
values,
|
||||||
}: ReactSpreadsheetImportContextProviderProps) => {
|
}: ReactSpreadsheetImportContextProviderProps<T>) => {
|
||||||
if (isUndefinedOrNull(values.spreadsheetImportFields)) {
|
if (isUndefinedOrNull(values.fields)) {
|
||||||
throw new Error('Fields must be provided to spreadsheet-import');
|
throw new Error('Fields must be provided to spreadsheet-import');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,43 +13,45 @@ import { act } from 'react';
|
|||||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
<RecoilRoot>{children}</RecoilRoot>
|
<RecoilRoot>{children}</RecoilRoot>
|
||||||
);
|
);
|
||||||
|
type SpreadsheetKey = 'spreadsheet_key';
|
||||||
|
|
||||||
export const mockedSpreadsheetOptions: SpreadsheetImportDialogOptions = {
|
export const mockedSpreadsheetOptions: SpreadsheetImportDialogOptions<SpreadsheetKey> =
|
||||||
onClose: () => {},
|
{
|
||||||
spreadsheetImportFields: [],
|
onClose: () => {},
|
||||||
uploadStepHook: async () => [],
|
fields: [],
|
||||||
selectHeaderStepHook: async (
|
uploadStepHook: async () => [],
|
||||||
headerValues: ImportedRow,
|
selectHeaderStepHook: async (
|
||||||
data: ImportedRow[],
|
headerValues: ImportedRow,
|
||||||
) => ({
|
data: ImportedRow[],
|
||||||
headerRow: headerValues,
|
) => ({
|
||||||
importedRows: data,
|
headerRow: headerValues,
|
||||||
}),
|
importedRows: data,
|
||||||
matchColumnsStepHook: async () => [],
|
}),
|
||||||
rowHook: () => ({ spreadsheet_key: 'rowHook' }),
|
matchColumnsStepHook: async () => [],
|
||||||
tableHook: () => [{ spreadsheet_key: 'tableHook' }],
|
rowHook: () => ({ spreadsheet_key: 'rowHook' }),
|
||||||
onSubmit: async () => {},
|
tableHook: () => [{ spreadsheet_key: 'tableHook' }],
|
||||||
allowInvalidSubmit: false,
|
onSubmit: async () => {},
|
||||||
customTheme: {},
|
allowInvalidSubmit: false,
|
||||||
maxRecords: 10,
|
customTheme: {},
|
||||||
maxFileSize: 50,
|
maxRecords: 10,
|
||||||
autoMapHeaders: true,
|
maxFileSize: 50,
|
||||||
autoMapDistance: 1,
|
autoMapHeaders: true,
|
||||||
initialStepState: {
|
autoMapDistance: 1,
|
||||||
type: SpreadsheetImportStepType.upload,
|
initialStepState: {
|
||||||
},
|
type: SpreadsheetImportStepType.upload,
|
||||||
dateFormat: 'MM/DD/YY',
|
},
|
||||||
parseRaw: true,
|
dateFormat: 'MM/DD/YY',
|
||||||
rtl: false,
|
parseRaw: true,
|
||||||
selectHeader: true,
|
rtl: false,
|
||||||
availableFieldMetadataItems: [],
|
selectHeader: true,
|
||||||
};
|
availableFieldMetadataItems: [],
|
||||||
|
};
|
||||||
|
|
||||||
describe('useSpreadsheetImport', () => {
|
describe('useSpreadsheetImport', () => {
|
||||||
it('should set isOpen to true, and update the options in the Recoil state', async () => {
|
it('should set isOpen to true, and update the options in the Recoil state', async () => {
|
||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() => ({
|
() => ({
|
||||||
useSpreadsheetImport: useOpenSpreadsheetImportDialog(),
|
useSpreadsheetImport: useOpenSpreadsheetImportDialog<SpreadsheetKey>(),
|
||||||
spreadsheetImportState: useRecoilState(spreadsheetImportDialogState)[0],
|
spreadsheetImportState: useRecoilState(spreadsheetImportDialogState)[0],
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
|
|||||||
@ -8,9 +8,8 @@ import { ImportedRow } from '@/spreadsheet-import/types';
|
|||||||
import { getMatchedColumnsWithFuse } from '@/spreadsheet-import/utils/getMatchedColumnsWithFuse';
|
import { getMatchedColumnsWithFuse } from '@/spreadsheet-import/utils/getMatchedColumnsWithFuse';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
export const useComputeColumnSuggestionsAndAutoMatch = () => {
|
export const useComputeColumnSuggestionsAndAutoMatch = <T extends string>() => {
|
||||||
const { spreadsheetImportFields: fields, autoMapHeaders } =
|
const { fields, autoMapHeaders } = useSpreadsheetImportInternal<T>();
|
||||||
useSpreadsheetImportInternal();
|
|
||||||
|
|
||||||
const computeColumnSuggestionsAndAutoMatch = useRecoilCallback(
|
const computeColumnSuggestionsAndAutoMatch = useRecoilCallback(
|
||||||
({ set, snapshot }) =>
|
({ set, snapshot }) =>
|
||||||
|
|||||||
@ -4,13 +4,13 @@ import { SPREADSHEET_IMPORT_MODAL_ID } from '@/spreadsheet-import/constants/Spre
|
|||||||
import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState';
|
import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState';
|
||||||
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
|
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
|
||||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||||
export const useOpenSpreadsheetImportDialog = () => {
|
export const useOpenSpreadsheetImportDialog = <T extends string>() => {
|
||||||
const setSpreadSheetImport = useSetRecoilState(spreadsheetImportDialogState);
|
const setSpreadSheetImport = useSetRecoilState(spreadsheetImportDialogState);
|
||||||
|
|
||||||
const { openModal } = useModal();
|
const { openModal } = useModal();
|
||||||
|
|
||||||
const openSpreadsheetImportDialog = (
|
const openSpreadsheetImportDialog = (
|
||||||
options: Omit<SpreadsheetImportDialogOptions, 'isOpen' | 'onClose'>,
|
options: Omit<SpreadsheetImportDialogOptions<T>, 'isOpen' | 'onClose'>,
|
||||||
) => {
|
) => {
|
||||||
openModal(SPREADSHEET_IMPORT_MODAL_ID);
|
openModal(SPREADSHEET_IMPORT_MODAL_ID);
|
||||||
setSpreadSheetImport({
|
setSpreadSheetImport({
|
||||||
|
|||||||
@ -5,10 +5,10 @@ import { RsiContext } from '@/spreadsheet-import/components/ReactSpreadsheetImpo
|
|||||||
import { defaultSpreadsheetImportProps } from '@/spreadsheet-import/provider/components/SpreadsheetImport';
|
import { defaultSpreadsheetImportProps } from '@/spreadsheet-import/provider/components/SpreadsheetImport';
|
||||||
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
|
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
|
||||||
|
|
||||||
export const useSpreadsheetImportInternal = () =>
|
export const useSpreadsheetImportInternal = <T extends string>() =>
|
||||||
useContext<
|
useContext<
|
||||||
SetRequired<
|
SetRequired<
|
||||||
SpreadsheetImportDialogOptions,
|
SpreadsheetImportDialogOptions<T>,
|
||||||
keyof typeof defaultSpreadsheetImportProps
|
keyof typeof defaultSpreadsheetImportProps
|
||||||
>
|
>
|
||||||
>(RsiContext);
|
>(RsiContext);
|
||||||
|
|||||||
@ -10,7 +10,9 @@ import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogMa
|
|||||||
import { useStepBar } from '@/ui/navigation/step-bar/hooks/useStepBar';
|
import { useStepBar } from '@/ui/navigation/step-bar/hooks/useStepBar';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
|
|
||||||
export const defaultSpreadsheetImportProps: Partial<SpreadsheetImportProps> = {
|
export const defaultSpreadsheetImportProps: Partial<
|
||||||
|
SpreadsheetImportProps<any>
|
||||||
|
> = {
|
||||||
autoMapHeaders: true,
|
autoMapHeaders: true,
|
||||||
allowInvalidSubmit: true,
|
allowInvalidSubmit: true,
|
||||||
autoMapDistance: 2,
|
autoMapDistance: 2,
|
||||||
@ -26,11 +28,13 @@ export const defaultSpreadsheetImportProps: Partial<SpreadsheetImportProps> = {
|
|||||||
maxRecords: SpreadsheetMaxRecordImportCapacity,
|
maxRecords: SpreadsheetMaxRecordImportCapacity,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const SpreadsheetImport = (props: SpreadsheetImportProps) => {
|
export const SpreadsheetImport = <T extends string>(
|
||||||
|
props: SpreadsheetImportProps<T>,
|
||||||
|
) => {
|
||||||
const mergedProps = {
|
const mergedProps = {
|
||||||
...defaultSpreadsheetImportProps,
|
...defaultSpreadsheetImportProps,
|
||||||
...props,
|
...props,
|
||||||
} as SpreadsheetImportProps;
|
} as SpreadsheetImportProps<T>;
|
||||||
|
|
||||||
const { enqueueDialog } = useDialogManager();
|
const { enqueueDialog } = useDialogManager();
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,19 @@
|
|||||||
import { createState } from 'twenty-ui/utilities';
|
import { createState } from 'twenty-ui/utilities';
|
||||||
import { SpreadsheetImportDialogOptions } from '../types';
|
import { SpreadsheetImportDialogOptions } from '../types';
|
||||||
|
|
||||||
export type SpreadsheetImportDialogState = {
|
export type SpreadsheetImportDialogState<T extends string> = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
isStepBarVisible: boolean;
|
isStepBarVisible: boolean;
|
||||||
options: Omit<SpreadsheetImportDialogOptions, 'isOpen' | 'onClose'> | null;
|
options: Omit<SpreadsheetImportDialogOptions<T>, 'isOpen' | 'onClose'> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const spreadsheetImportDialogState =
|
export const spreadsheetImportDialogState = createState<
|
||||||
createState<SpreadsheetImportDialogState>({
|
SpreadsheetImportDialogState<any>
|
||||||
key: 'spreadsheetImportDialogState',
|
>({
|
||||||
defaultValue: {
|
key: 'spreadsheetImportDialogState',
|
||||||
isOpen: false,
|
defaultValue: {
|
||||||
isStepBarVisible: true,
|
isOpen: false,
|
||||||
options: null,
|
isStepBarVisible: true,
|
||||||
},
|
options: null,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton';
|
import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton';
|
||||||
|
import { useHideStepBar } from '@/spreadsheet-import/hooks/useHideStepBar';
|
||||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||||
import { spreadsheetImportCreatedRecordsProgressState } from '@/spreadsheet-import/states/spreadsheetImportCreatedRecordsProgressState';
|
import { spreadsheetImportCreatedRecordsProgressState } from '@/spreadsheet-import/states/spreadsheetImportCreatedRecordsProgressState';
|
||||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||||
@ -37,6 +38,9 @@ type ImportDataStepProps = {
|
|||||||
export const ImportDataStep = ({
|
export const ImportDataStep = ({
|
||||||
recordsToImportCount,
|
recordsToImportCount,
|
||||||
}: ImportDataStepProps) => {
|
}: ImportDataStepProps) => {
|
||||||
|
const hideStepBar = useHideStepBar();
|
||||||
|
hideStepBar();
|
||||||
|
|
||||||
const { onClose } = useSpreadsheetImportInternal();
|
const { onClose } = useSpreadsheetImportInternal();
|
||||||
const spreadsheetImportCreatedRecordsProgress = useRecoilValue(
|
const spreadsheetImportCreatedRecordsProgress = useRecoilValue(
|
||||||
spreadsheetImportCreatedRecordsProgressState,
|
spreadsheetImportCreatedRecordsProgressState,
|
||||||
|
|||||||
@ -64,7 +64,7 @@ export type MatchColumnsStepProps = {
|
|||||||
onError: (message: string) => void;
|
onError: (message: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MatchColumnsStep = ({
|
export const MatchColumnsStep = <T extends string>({
|
||||||
data,
|
data,
|
||||||
headerValues,
|
headerValues,
|
||||||
onBack,
|
onBack,
|
||||||
@ -76,7 +76,7 @@ export const MatchColumnsStep = ({
|
|||||||
}: MatchColumnsStepProps) => {
|
}: MatchColumnsStepProps) => {
|
||||||
const { enqueueDialog } = useDialogManager();
|
const { enqueueDialog } = useDialogManager();
|
||||||
const dataExample = data.slice(0, 2);
|
const dataExample = data.slice(0, 2);
|
||||||
const { spreadsheetImportFields: fields } = useSpreadsheetImportInternal();
|
const { fields } = useSpreadsheetImportInternal<T>();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [columns, setColumns] = useRecoilState(
|
const [columns, setColumns] = useRecoilState(
|
||||||
initialComputedColumnsSelector(headerValues),
|
initialComputedColumnsSelector(headerValues),
|
||||||
@ -90,7 +90,7 @@ export const MatchColumnsStep = ({
|
|||||||
(columnIndex: number) => {
|
(columnIndex: number) => {
|
||||||
setColumns(
|
setColumns(
|
||||||
columns.map((column, index) =>
|
columns.map((column, index) =>
|
||||||
columnIndex === index ? setIgnoreColumn(column) : column,
|
columnIndex === index ? setIgnoreColumn<string>(column) : column,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -109,7 +109,7 @@ export const MatchColumnsStep = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(value: string, columnIndex: number) => {
|
(value: T, columnIndex: number) => {
|
||||||
if (value === DO_NOT_IMPORT_OPTION_KEY) {
|
if (value === DO_NOT_IMPORT_OPTION_KEY) {
|
||||||
if (columns[columnIndex].type === SpreadsheetColumnType.ignored) {
|
if (columns[columnIndex].type === SpreadsheetColumnType.ignored) {
|
||||||
onRevertIgnore(columnIndex);
|
onRevertIgnore(columnIndex);
|
||||||
@ -119,12 +119,12 @@ export const MatchColumnsStep = ({
|
|||||||
} else {
|
} else {
|
||||||
const field = fields.find(
|
const field = fields.find(
|
||||||
(field) => field.key === value,
|
(field) => field.key === value,
|
||||||
) as unknown as SpreadsheetImportField;
|
) as unknown as SpreadsheetImportField<T>;
|
||||||
const existingFieldIndex = columns.findIndex(
|
const existingFieldIndex = columns.findIndex(
|
||||||
(column) => 'value' in column && column.value === field.key,
|
(column) => 'value' in column && column.value === field.key,
|
||||||
);
|
);
|
||||||
setColumns(
|
setColumns(
|
||||||
columns.map<SpreadsheetColumn>((column, index) => {
|
columns.map<SpreadsheetColumn<string>>((column, index) => {
|
||||||
if (columnIndex === index) {
|
if (columnIndex === index) {
|
||||||
return setColumn(column, field, data);
|
return setColumn(column, field, data);
|
||||||
} else if (index === existingFieldIndex) {
|
} else if (index === existingFieldIndex) {
|
||||||
@ -141,9 +141,9 @@ export const MatchColumnsStep = ({
|
|||||||
|
|
||||||
const handleContinue = useCallback(
|
const handleContinue = useCallback(
|
||||||
async (
|
async (
|
||||||
values: ImportedStructuredRow[],
|
values: ImportedStructuredRow<string>[],
|
||||||
rawData: ImportedRow[],
|
rawData: ImportedRow[],
|
||||||
columns: SpreadsheetColumns,
|
columns: SpreadsheetColumns<string>,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const data = await matchColumnsStepHook(values, rawData, columns);
|
const data = await matchColumnsStepHook(values, rawData, columns);
|
||||||
|
|||||||
@ -82,28 +82,28 @@ const StyledGridHeader = styled.div<PositionProps>`
|
|||||||
padding-right: ${({ theme }) => theme.spacing(4)};
|
padding-right: ${({ theme }) => theme.spacing(4)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type ColumnGridProps = {
|
type ColumnGridProps<T extends string> = {
|
||||||
columns: SpreadsheetColumns;
|
columns: SpreadsheetColumns<T>;
|
||||||
renderUserColumn: (
|
renderUserColumn: (
|
||||||
columns: SpreadsheetColumns,
|
columns: SpreadsheetColumns<T>,
|
||||||
columnIndex: number,
|
columnIndex: number,
|
||||||
) => React.ReactNode;
|
) => React.ReactNode;
|
||||||
renderTemplateColumn: (
|
renderTemplateColumn: (
|
||||||
columns: SpreadsheetColumns,
|
columns: SpreadsheetColumns<T>,
|
||||||
columnIndex: number,
|
columnIndex: number,
|
||||||
) => React.ReactNode;
|
) => React.ReactNode;
|
||||||
renderUnmatchedColumn: (
|
renderUnmatchedColumn: (
|
||||||
columns: SpreadsheetColumns,
|
columns: SpreadsheetColumns<T>,
|
||||||
columnIndex: number,
|
columnIndex: number,
|
||||||
) => React.ReactNode;
|
) => React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ColumnGrid = ({
|
export const ColumnGrid = <T extends string>({
|
||||||
columns,
|
columns,
|
||||||
renderUserColumn,
|
renderUserColumn,
|
||||||
renderTemplateColumn,
|
renderTemplateColumn,
|
||||||
renderUnmatchedColumn,
|
renderUnmatchedColumn,
|
||||||
}: ColumnGridProps) => {
|
}: ColumnGridProps<T>) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledGridContainer>
|
<StyledGridContainer>
|
||||||
|
|||||||
@ -16,20 +16,20 @@ const StyledIconChevronDown = styled(IconChevronDown)`
|
|||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type SubMatchingSelectDropdownButtonProps = {
|
export type SubMatchingSelectDropdownButtonProps<T> = {
|
||||||
option: SpreadsheetMatchedOptions | Partial<SpreadsheetMatchedOptions>;
|
option: SpreadsheetMatchedOptions<T> | Partial<SpreadsheetMatchedOptions<T>>;
|
||||||
column:
|
column:
|
||||||
| SpreadsheetMatchedSelectColumn
|
| SpreadsheetMatchedSelectColumn<T>
|
||||||
| SpreadsheetMatchedSelectOptionsColumn;
|
| SpreadsheetMatchedSelectOptionsColumn<T>;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SubMatchingSelectDropdownButton = ({
|
export const SubMatchingSelectDropdownButton = <T extends string>({
|
||||||
option,
|
option,
|
||||||
column,
|
column,
|
||||||
placeholder,
|
placeholder,
|
||||||
}: SubMatchingSelectDropdownButtonProps) => {
|
}: SubMatchingSelectDropdownButtonProps<T>) => {
|
||||||
const { spreadsheetImportFields: fields } = useSpreadsheetImportInternal();
|
const { fields } = useSpreadsheetImportInternal<T>();
|
||||||
const options = getFieldOptions(fields, column.value) as SelectOption[];
|
const options = getFieldOptions(fields, column.value) as SelectOption[];
|
||||||
const value = options.find((opt) => opt.value === option.value);
|
const value = options.find((opt) => opt.value === option.value);
|
||||||
|
|
||||||
|
|||||||
@ -15,23 +15,23 @@ const StyledRowContainer = styled.div`
|
|||||||
padding-bottom: ${({ theme }) => theme.spacing(1)};
|
padding-bottom: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface SubMatchingSelectRowProps {
|
interface SubMatchingSelectRowProps<T> {
|
||||||
option: SpreadsheetMatchedOptions | Partial<SpreadsheetMatchedOptions>;
|
option: SpreadsheetMatchedOptions<T> | Partial<SpreadsheetMatchedOptions<T>>;
|
||||||
column:
|
column:
|
||||||
| SpreadsheetMatchedSelectColumn
|
| SpreadsheetMatchedSelectColumn<T>
|
||||||
| SpreadsheetMatchedSelectOptionsColumn;
|
| SpreadsheetMatchedSelectOptionsColumn<T>;
|
||||||
onSubChange: (val: string, index: number, option: string) => void;
|
onSubChange: (val: T, index: number, option: string) => void;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
selectedOption?:
|
selectedOption?:
|
||||||
| SpreadsheetMatchedOptions
|
| SpreadsheetMatchedOptions<T>
|
||||||
| Partial<SpreadsheetMatchedOptions>;
|
| Partial<SpreadsheetMatchedOptions<T>>;
|
||||||
}
|
}
|
||||||
export const SubMatchingSelectRow = ({
|
export const SubMatchingSelectRow = <T extends string>({
|
||||||
option,
|
option,
|
||||||
column,
|
column,
|
||||||
onSubChange,
|
onSubChange,
|
||||||
placeholder,
|
placeholder,
|
||||||
}: SubMatchingSelectRowProps) => {
|
}: SubMatchingSelectRowProps<T>) => {
|
||||||
return (
|
return (
|
||||||
<StyledRowContainer>
|
<StyledRowContainer>
|
||||||
<SubMatchingSelectRowLeftSelect option={option} />
|
<SubMatchingSelectRowLeftSelect option={option} />
|
||||||
|
|||||||
@ -15,13 +15,13 @@ const StyledControlLabel = styled.div`
|
|||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type SubMatchingSelectRowLeftSelectProps = {
|
export type SubMatchingSelectRowLeftSelectProps<T> = {
|
||||||
option: SpreadsheetMatchedOptions | Partial<SpreadsheetMatchedOptions>;
|
option: SpreadsheetMatchedOptions<T> | Partial<SpreadsheetMatchedOptions<T>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SubMatchingSelectRowLeftSelect = ({
|
export const SubMatchingSelectRowLeftSelect = <T extends string>({
|
||||||
option,
|
option,
|
||||||
}: SubMatchingSelectRowLeftSelectProps) => {
|
}: SubMatchingSelectRowLeftSelectProps<T>) => {
|
||||||
return (
|
return (
|
||||||
<SubMatchingSelectControlContainer cursor="default">
|
<SubMatchingSelectControlContainer cursor="default">
|
||||||
<StyledControlLabel>
|
<StyledControlLabel>
|
||||||
|
|||||||
@ -18,34 +18,34 @@ const StyledDropdownContainer = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface SubMatchingSelectRowRightDropdownProps {
|
interface SubMatchingSelectRowRightDropdownProps<T> {
|
||||||
option: SpreadsheetMatchedOptions | Partial<SpreadsheetMatchedOptions>;
|
option: SpreadsheetMatchedOptions<T> | Partial<SpreadsheetMatchedOptions<T>>;
|
||||||
column:
|
column:
|
||||||
| SpreadsheetMatchedSelectColumn
|
| SpreadsheetMatchedSelectColumn<T>
|
||||||
| SpreadsheetMatchedSelectOptionsColumn;
|
| SpreadsheetMatchedSelectOptionsColumn<T>;
|
||||||
onSubChange: (val: string, index: number, option: string) => void;
|
onSubChange: (val: T, index: number, option: string) => void;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
selectedOption?:
|
selectedOption?:
|
||||||
| SpreadsheetMatchedOptions
|
| SpreadsheetMatchedOptions<T>
|
||||||
| Partial<SpreadsheetMatchedOptions>;
|
| Partial<SpreadsheetMatchedOptions<T>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SubMatchingSelectRowRightDropdown = ({
|
export const SubMatchingSelectRowRightDropdown = <T extends string>({
|
||||||
option,
|
option,
|
||||||
column,
|
column,
|
||||||
onSubChange,
|
onSubChange,
|
||||||
placeholder,
|
placeholder,
|
||||||
}: SubMatchingSelectRowRightDropdownProps) => {
|
}: SubMatchingSelectRowRightDropdownProps<T>) => {
|
||||||
const dropdownId = `sub-matching-select-dropdown-${option.entry}`;
|
const dropdownId = `sub-matching-select-dropdown-${option.entry}`;
|
||||||
|
|
||||||
const { closeDropdown } = useCloseDropdown();
|
const { closeDropdown } = useCloseDropdown();
|
||||||
|
|
||||||
const { spreadsheetImportFields: fields } = useSpreadsheetImportInternal();
|
const { fields } = useSpreadsheetImportInternal<T>();
|
||||||
const options = getFieldOptions(fields, column.value) as SelectOption[];
|
const options = getFieldOptions(fields, column.value) as SelectOption[];
|
||||||
const value = options.find((opt) => opt.value === option.value);
|
const value = options.find((opt) => opt.value === option.value);
|
||||||
|
|
||||||
const handleSelect = (selectedOption: SelectOption) => {
|
const handleSelect = (selectedOption: SelectOption) => {
|
||||||
onSubChange(selectedOption.value, column.index, option.entry ?? '');
|
onSubChange(selectedOption.value as T, column.index, option.entry ?? '');
|
||||||
closeDropdown(dropdownId);
|
closeDropdown(dropdownId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpre
|
|||||||
import { suggestedFieldsByColumnHeaderState } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/states/suggestedFieldsByColumnHeaderState';
|
import { suggestedFieldsByColumnHeaderState } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/states/suggestedFieldsByColumnHeaderState';
|
||||||
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
||||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||||
import { spreadsheetImportBuildFieldOptions } from '@/spreadsheet-import/utils/spreadsheetImportBuildFieldOptions';
|
import { spreadsheetBuildFieldOptions } from '@/spreadsheet-import/utils/spreadsheetBuildFieldOptions';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { IconForbid } from 'twenty-ui/display';
|
import { IconForbid } from 'twenty-ui/display';
|
||||||
@ -25,18 +25,18 @@ const StyledErrorMessage = styled.span`
|
|||||||
margin-top: ${({ theme }) => theme.spacing(1)};
|
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type TemplateColumnProps = {
|
type TemplateColumnProps<T extends string> = {
|
||||||
columns: SpreadsheetColumns;
|
columns: SpreadsheetColumns<string>;
|
||||||
columnIndex: number;
|
columnIndex: number;
|
||||||
onChange: (val: string, index: number) => void;
|
onChange: (val: T, index: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TemplateColumn = ({
|
export const TemplateColumn = <T extends string>({
|
||||||
columns,
|
columns,
|
||||||
columnIndex,
|
columnIndex,
|
||||||
onChange,
|
onChange,
|
||||||
}: TemplateColumnProps) => {
|
}: TemplateColumnProps<T>) => {
|
||||||
const { spreadsheetImportFields: fields } = useSpreadsheetImportInternal();
|
const { fields } = useSpreadsheetImportInternal<T>();
|
||||||
const suggestedFieldsByColumnHeader = useRecoilValue(
|
const suggestedFieldsByColumnHeader = useRecoilValue(
|
||||||
suggestedFieldsByColumnHeaderState,
|
suggestedFieldsByColumnHeaderState,
|
||||||
);
|
);
|
||||||
@ -46,8 +46,8 @@ export const TemplateColumn = ({
|
|||||||
|
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
const fieldOptions = spreadsheetImportBuildFieldOptions(fields, columns);
|
const fieldOptions = spreadsheetBuildFieldOptions(fields, columns);
|
||||||
const suggestedFieldOptions = spreadsheetImportBuildFieldOptions(
|
const suggestedFieldOptions = spreadsheetBuildFieldOptions(
|
||||||
suggestedFieldsByColumnHeader[column.header] ?? [],
|
suggestedFieldsByColumnHeader[column.header] ?? [],
|
||||||
columns,
|
columns,
|
||||||
);
|
);
|
||||||
@ -74,7 +74,7 @@ export const TemplateColumn = ({
|
|||||||
<MatchColumnToFieldSelect
|
<MatchColumnToFieldSelect
|
||||||
placeholder={t`Select column...`}
|
placeholder={t`Select column...`}
|
||||||
value={isIgnored ? ignoreValue : selectValue}
|
value={isIgnored ? ignoreValue : selectValue}
|
||||||
onChange={(value) => onChange(value?.value as string, column.index)}
|
onChange={(value) => onChange(value?.value as T, column.index)}
|
||||||
options={selectOptions}
|
options={selectOptions}
|
||||||
suggestedOptions={suggestedFieldOptions}
|
suggestedOptions={suggestedFieldOptions}
|
||||||
columnIndex={column.index.toString()}
|
columnIndex={column.index.toString()}
|
||||||
|
|||||||
@ -11,9 +11,9 @@ import { useState } from 'react';
|
|||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||||
|
|
||||||
const getExpandableContainerTitle = (
|
const getExpandableContainerTitle = <T extends string>(
|
||||||
fields: SpreadsheetImportFields,
|
fields: SpreadsheetImportFields<T>,
|
||||||
column: SpreadsheetColumn,
|
column: SpreadsheetColumn<T>,
|
||||||
) => {
|
) => {
|
||||||
const fieldLabel = fields.find(
|
const fieldLabel = fields.find(
|
||||||
(field) => 'value' in column && field.key === column.value,
|
(field) => 'value' in column && field.key === column.value,
|
||||||
@ -25,10 +25,10 @@ const getExpandableContainerTitle = (
|
|||||||
} Unmatched)`;
|
} Unmatched)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UnmatchColumnProps = {
|
type UnmatchColumnProps<T extends string> = {
|
||||||
columns: SpreadsheetColumns;
|
columns: SpreadsheetColumns<T>;
|
||||||
columnIndex: number;
|
columnIndex: number;
|
||||||
onSubChange: (val: string, index: number, option: string) => void;
|
onSubChange: (val: T, index: number, option: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -44,12 +44,12 @@ const StyledContentWrapper = styled.div`
|
|||||||
padding-bottom: ${({ theme }) => theme.spacing(4)};
|
padding-bottom: ${({ theme }) => theme.spacing(4)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const UnmatchColumn = ({
|
export const UnmatchColumn = <T extends string>({
|
||||||
columns,
|
columns,
|
||||||
columnIndex,
|
columnIndex,
|
||||||
onSubChange,
|
onSubChange,
|
||||||
}: UnmatchColumnProps) => {
|
}: UnmatchColumnProps<T>) => {
|
||||||
const { spreadsheetImportFields: fields } = useSpreadsheetImportInternal();
|
const { fields } = useSpreadsheetImportInternal<T>();
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const column = columns[columnIndex];
|
const column = columns[columnIndex];
|
||||||
const isSelect = 'matchedOptions' in column;
|
const isSelect = 'matchedOptions' in column;
|
||||||
|
|||||||
@ -29,15 +29,15 @@ const StyledExample = styled.span`
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type UserTableColumnProps = {
|
type UserTableColumnProps<T extends string> = {
|
||||||
column: SpreadsheetColumn;
|
column: SpreadsheetColumn<T>;
|
||||||
importedRow: ImportedRow;
|
importedRow: ImportedRow;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserTableColumn = ({
|
export const UserTableColumn = <T extends string>({
|
||||||
column,
|
column,
|
||||||
importedRow,
|
importedRow,
|
||||||
}: UserTableColumnProps) => {
|
}: UserTableColumnProps<T>) => {
|
||||||
const { header } = column;
|
const { header } = column;
|
||||||
const firstDefinedValue = importedRow.find(isDefined);
|
const firstDefinedValue = importedRow.find(isDefined);
|
||||||
|
|
||||||
|
|||||||
@ -5,18 +5,18 @@ import { atom, selectorFamily } from 'recoil';
|
|||||||
|
|
||||||
export const matchColumnsState = atom({
|
export const matchColumnsState = atom({
|
||||||
key: 'MatchColumnsState',
|
key: 'MatchColumnsState',
|
||||||
default: [] as SpreadsheetColumns,
|
default: [] as SpreadsheetColumns<string>,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const initialComputedColumnsSelector = selectorFamily<
|
export const initialComputedColumnsSelector = selectorFamily<
|
||||||
SpreadsheetColumns,
|
SpreadsheetColumns<string>,
|
||||||
ImportedRow
|
ImportedRow
|
||||||
>({
|
>({
|
||||||
key: 'initialComputedColumnsSelector',
|
key: 'initialComputedColumnsSelector',
|
||||||
get:
|
get:
|
||||||
(headerValues: ImportedRow) =>
|
(headerValues: ImportedRow) =>
|
||||||
({ get }) => {
|
({ get }) => {
|
||||||
const currentState = get(matchColumnsState) as SpreadsheetColumns;
|
const currentState = get(matchColumnsState) as SpreadsheetColumns<string>;
|
||||||
if (currentState.length === 0) {
|
if (currentState.length === 0) {
|
||||||
// Do not remove spread, it indexes empty array elements, otherwise map() skips over them
|
// Do not remove spread, it indexes empty array elements, otherwise map() skips over them
|
||||||
const initialState = ([...headerValues] as string[]).map(
|
const initialState = ([...headerValues] as string[]).map(
|
||||||
@ -26,7 +26,7 @@ export const initialComputedColumnsSelector = selectorFamily<
|
|||||||
header: value ?? '',
|
header: value ?? '',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return initialState as SpreadsheetColumns;
|
return initialState as SpreadsheetColumns<string>;
|
||||||
} else {
|
} else {
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
@ -34,6 +34,6 @@ export const initialComputedColumnsSelector = selectorFamily<
|
|||||||
set:
|
set:
|
||||||
() =>
|
() =>
|
||||||
({ set }, newValue) => {
|
({ set }, newValue) => {
|
||||||
set(matchColumnsState, newValue as SpreadsheetColumns);
|
set(matchColumnsState, newValue as SpreadsheetColumns<string>);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,5 +3,5 @@ import { createState } from 'twenty-ui/utilities';
|
|||||||
|
|
||||||
export const suggestedFieldsByColumnHeaderState = createState({
|
export const suggestedFieldsByColumnHeaderState = createState({
|
||||||
key: 'suggestedFieldsByColumnHeaderState',
|
key: 'suggestedFieldsByColumnHeaderState',
|
||||||
defaultValue: {} as Record<string, SpreadsheetImportField[]>,
|
defaultValue: {} as Record<string, SpreadsheetImportField<string>[]>,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,58 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
// @ts-expect-error // Todo: remove usage of react-data-grid
|
||||||
|
import { Column } from 'react-data-grid';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||||
|
import { AppTooltip } from 'twenty-ui/display';
|
||||||
|
|
||||||
|
const StyledHeaderContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledHeaderLabel = styled.span`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledDefaultContainer = styled.div`
|
||||||
|
min-height: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const generateColumns = <T extends string>(
|
||||||
|
fields: SpreadsheetImportFields<T>,
|
||||||
|
) =>
|
||||||
|
fields.map(
|
||||||
|
(column): Column<any> => ({
|
||||||
|
key: column.key,
|
||||||
|
name: column.label,
|
||||||
|
minWidth: 150,
|
||||||
|
headerRenderer: () => (
|
||||||
|
<StyledHeaderContainer>
|
||||||
|
<StyledHeaderLabel id={`${column.key}`}>
|
||||||
|
{column.label}
|
||||||
|
</StyledHeaderLabel>
|
||||||
|
{column.description &&
|
||||||
|
createPortal(
|
||||||
|
<AppTooltip
|
||||||
|
anchorSelect={`#${column.key}`}
|
||||||
|
place="top"
|
||||||
|
content={column.description}
|
||||||
|
/>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</StyledHeaderContainer>
|
||||||
|
),
|
||||||
|
formatter: ({ row }: any) => (
|
||||||
|
<StyledDefaultContainer>{row[column.key]}</StyledDefaultContainer>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||||
import { spreadsheetImportFilterAvailableFieldMetadataItems } from '@/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems';
|
import { spreadsheetImportFilterAvailableFieldMetadataItems } from '@/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts';
|
||||||
import { getCompositeSubFieldLabelWithFieldLabel } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldLabelWithFieldLabel';
|
|
||||||
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
||||||
import { SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs';
|
import { SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs';
|
||||||
import { escapeCSVValue } from '@/spreadsheet-import/utils/escapeCSVValue';
|
import { escapeCSVValue } from '@/spreadsheet-import/utils/escapeCSVValue';
|
||||||
@ -59,8 +58,8 @@ export const useDownloadFakeRecords = () => {
|
|||||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[field.type].exampleValues;
|
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[field.type].exampleValues;
|
||||||
|
|
||||||
headerRow.push(
|
headerRow.push(
|
||||||
...subFields.map(({ subFieldLabel }) =>
|
...subFields.map(
|
||||||
getCompositeSubFieldLabelWithFieldLabel(field, subFieldLabel),
|
({ subFieldLabel }) => `${field.label} / ${subFieldLabel}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { SpreadsheetImportTable } from '@/spreadsheet-import/components/SpreadsheetImportTable';
|
import { SpreadsheetImportTable } from '@/spreadsheet-import/components/SpreadsheetImportTable';
|
||||||
import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton';
|
import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton';
|
||||||
import { useHideStepBar } from '@/spreadsheet-import/hooks/useHideStepBar';
|
|
||||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||||
import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep';
|
import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep';
|
||||||
import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType';
|
import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType';
|
||||||
@ -92,36 +91,30 @@ const StyledNoRowsWithErrorsContainer = styled.div`
|
|||||||
margin: auto 0;
|
margin: auto 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type ValidationStepProps = {
|
type ValidationStepProps<T extends string> = {
|
||||||
initialData: ImportedStructuredRow[];
|
initialData: ImportedStructuredRow<T>[];
|
||||||
importedColumns: SpreadsheetColumns;
|
importedColumns: SpreadsheetColumns<string>;
|
||||||
file: File;
|
file: File;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
setCurrentStepState: Dispatch<SetStateAction<SpreadsheetImportStep>>;
|
setCurrentStepState: Dispatch<SetStateAction<SpreadsheetImportStep>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ValidationStep = ({
|
export const ValidationStep = <T extends string>({
|
||||||
initialData,
|
initialData,
|
||||||
importedColumns,
|
importedColumns,
|
||||||
file,
|
file,
|
||||||
setCurrentStepState,
|
setCurrentStepState,
|
||||||
onBack,
|
onBack,
|
||||||
}: ValidationStepProps) => {
|
}: ValidationStepProps<T>) => {
|
||||||
const hideStepBar = useHideStepBar();
|
|
||||||
const { enqueueDialog } = useDialogManager();
|
const { enqueueDialog } = useDialogManager();
|
||||||
const {
|
const { fields, onClose, onSubmit, rowHook, tableHook } =
|
||||||
spreadsheetImportFields: fields,
|
useSpreadsheetImportInternal<T>();
|
||||||
onClose,
|
|
||||||
onSubmit,
|
|
||||||
rowHook,
|
|
||||||
tableHook,
|
|
||||||
} = useSpreadsheetImportInternal();
|
|
||||||
|
|
||||||
const [data, setData] = useState<
|
const [data, setData] = useState<
|
||||||
(ImportedStructuredRow & ImportedStructuredRowMetadata)[]
|
(ImportedStructuredRow<T> & ImportedStructuredRowMetadata)[]
|
||||||
>(
|
>(
|
||||||
useMemo(
|
useMemo(
|
||||||
() => addErrorsAndRunHooks(initialData, fields, rowHook, tableHook),
|
() => addErrorsAndRunHooks<T>(initialData, fields, rowHook, tableHook),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[],
|
[],
|
||||||
),
|
),
|
||||||
@ -133,7 +126,7 @@ export const ValidationStep = ({
|
|||||||
|
|
||||||
const updateData = useCallback(
|
const updateData = useCallback(
|
||||||
(rows: typeof data) => {
|
(rows: typeof data) => {
|
||||||
setData(addErrorsAndRunHooks(rows, fields, rowHook, tableHook));
|
setData(addErrorsAndRunHooks<T>(rows, fields, rowHook, tableHook));
|
||||||
},
|
},
|
||||||
[setData, rowHook, tableHook, fields],
|
[setData, rowHook, tableHook, fields],
|
||||||
);
|
);
|
||||||
@ -212,7 +205,8 @@ export const ValidationStep = ({
|
|||||||
}, [data, filterByErrors]);
|
}, [data, filterByErrors]);
|
||||||
|
|
||||||
const rowKeyGetter = useCallback(
|
const rowKeyGetter = useCallback(
|
||||||
(row: ImportedStructuredRow & ImportedStructuredRowMetadata) => row.__index,
|
(row: ImportedStructuredRow<T> & ImportedStructuredRowMetadata) =>
|
||||||
|
row.__index,
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -224,29 +218,28 @@ export const ValidationStep = ({
|
|||||||
for (const key in __errors) {
|
for (const key in __errors) {
|
||||||
if (__errors[key].level === 'error') {
|
if (__errors[key].level === 'error') {
|
||||||
acc.invalidStructuredRows.push(
|
acc.invalidStructuredRows.push(
|
||||||
values as unknown as ImportedStructuredRow,
|
values as unknown as ImportedStructuredRow<T>,
|
||||||
);
|
);
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
acc.validStructuredRows.push(
|
acc.validStructuredRows.push(
|
||||||
values as unknown as ImportedStructuredRow,
|
values as unknown as ImportedStructuredRow<T>,
|
||||||
);
|
);
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
validStructuredRows: [] as ImportedStructuredRow[],
|
validStructuredRows: [] as ImportedStructuredRow<T>[],
|
||||||
invalidStructuredRows: [] as ImportedStructuredRow[],
|
invalidStructuredRows: [] as ImportedStructuredRow<T>[],
|
||||||
allStructuredRows: data,
|
allStructuredRows: data,
|
||||||
} satisfies SpreadsheetImportImportValidationResult,
|
} satisfies SpreadsheetImportImportValidationResult<T>,
|
||||||
);
|
);
|
||||||
|
|
||||||
setCurrentStepState({
|
setCurrentStepState({
|
||||||
type: SpreadsheetImportStepType.importData,
|
type: SpreadsheetImportStepType.importData,
|
||||||
recordsToImportCount: calculatedData.validStructuredRows.length,
|
recordsToImportCount: calculatedData.validStructuredRows.length,
|
||||||
});
|
});
|
||||||
hideStepBar();
|
|
||||||
|
|
||||||
await onSubmit(calculatedData, file);
|
await onSubmit(calculatedData, file);
|
||||||
onClose();
|
onClose();
|
||||||
|
|||||||
@ -71,9 +71,9 @@ const formatSafeId = (columnKey: string) => {
|
|||||||
return camelCase(columnKey.replace('(', '').replace(')', ''));
|
return camelCase(columnKey.replace('(', '').replace(')', ''));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generateColumns = (
|
export const generateColumns = <T extends string>(
|
||||||
fields: SpreadsheetImportFields,
|
fields: SpreadsheetImportFields<T>,
|
||||||
): Column<ImportedStructuredRow & ImportedStructuredRowMetadata>[] => [
|
): Column<ImportedStructuredRow<T> & ImportedStructuredRowMetadata>[] => [
|
||||||
{
|
{
|
||||||
key: SELECT_COLUMN_KEY,
|
key: SELECT_COLUMN_KEY,
|
||||||
name: '',
|
name: '',
|
||||||
@ -108,7 +108,7 @@ export const generateColumns = (
|
|||||||
...fields.map(
|
...fields.map(
|
||||||
(
|
(
|
||||||
column,
|
column,
|
||||||
): Column<ImportedStructuredRow & ImportedStructuredRowMetadata> => ({
|
): Column<ImportedStructuredRow<T> & ImportedStructuredRowMetadata> => ({
|
||||||
key: column.key,
|
key: column.key,
|
||||||
name: column.label,
|
name: column.label,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
@ -132,7 +132,7 @@ export const generateColumns = (
|
|||||||
editable: column.fieldType.type !== 'checkbox',
|
editable: column.fieldType.type !== 'checkbox',
|
||||||
// Todo: remove usage of react-data-grid
|
// Todo: remove usage of react-data-grid
|
||||||
editor: ({ row, onRowChange, onClose }: any) => {
|
editor: ({ row, onRowChange, onClose }: any) => {
|
||||||
const columnKey = column.key as keyof (ImportedStructuredRow &
|
const columnKey = column.key as keyof (ImportedStructuredRow<T> &
|
||||||
ImportedStructuredRowMetadata);
|
ImportedStructuredRowMetadata);
|
||||||
let component;
|
let component;
|
||||||
|
|
||||||
@ -166,7 +166,7 @@ export const generateColumns = (
|
|||||||
},
|
},
|
||||||
// Todo: remove usage of react-data-grid
|
// Todo: remove usage of react-data-grid
|
||||||
formatter: ({ row, onRowChange }: { row: any; onRowChange: any }) => {
|
formatter: ({ row, onRowChange }: { row: any; onRowChange: any }) => {
|
||||||
const columnKey = column.key as keyof (ImportedStructuredRow &
|
const columnKey = column.key as keyof (ImportedStructuredRow<T> &
|
||||||
ImportedStructuredRowMetadata);
|
ImportedStructuredRowMetadata);
|
||||||
let component;
|
let component;
|
||||||
|
|
||||||
@ -197,7 +197,7 @@ export const generateColumns = (
|
|||||||
id={formatSafeId(`${columnKey}-${row.__index}`)}
|
id={formatSafeId(`${columnKey}-${row.__index}`)}
|
||||||
>
|
>
|
||||||
{column.fieldType.options.find(
|
{column.fieldType.options.find(
|
||||||
(option) => option.value === row[columnKey],
|
(option) => option.value === row[columnKey as T],
|
||||||
)?.label || null}
|
)?.label || null}
|
||||||
</StyledDefaultContainer>
|
</StyledDefaultContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export type SpreadsheetImportStep =
|
|||||||
| {
|
| {
|
||||||
type: SpreadsheetImportStepType.validateData;
|
type: SpreadsheetImportStepType.validateData;
|
||||||
data: any[];
|
data: any[];
|
||||||
importedColumns: SpreadsheetColumns;
|
importedColumns: SpreadsheetColumns<string>;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: SpreadsheetImportStepType.loading;
|
type: SpreadsheetImportStepType.loading;
|
||||||
|
|||||||
@ -13,49 +13,49 @@ type SpreadsheetIgnoredColumn = {
|
|||||||
header: string;
|
header: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SpreadsheetMatchedColumn = {
|
type SpreadsheetMatchedColumn<T> = {
|
||||||
type: SpreadsheetColumnType.matched;
|
type: SpreadsheetColumnType.matched;
|
||||||
index: number;
|
index: number;
|
||||||
header: string;
|
header: string;
|
||||||
value: string;
|
value: T;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SpreadsheetMatchedSwitchColumn = {
|
type SpreadsheetMatchedSwitchColumn<T> = {
|
||||||
type: SpreadsheetColumnType.matchedCheckbox;
|
type: SpreadsheetColumnType.matchedCheckbox;
|
||||||
index: number;
|
index: number;
|
||||||
header: string;
|
header: string;
|
||||||
value: string;
|
value: T;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SpreadsheetMatchedSelectColumn = {
|
export type SpreadsheetMatchedSelectColumn<T> = {
|
||||||
type: SpreadsheetColumnType.matchedSelect;
|
type: SpreadsheetColumnType.matchedSelect;
|
||||||
index: number;
|
index: number;
|
||||||
header: string;
|
header: string;
|
||||||
value: string;
|
value: T;
|
||||||
matchedOptions: Partial<SpreadsheetMatchedOptions>[];
|
matchedOptions: Partial<SpreadsheetMatchedOptions<T>>[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SpreadsheetMatchedSelectOptionsColumn = {
|
export type SpreadsheetMatchedSelectOptionsColumn<T> = {
|
||||||
type: SpreadsheetColumnType.matchedSelectOptions;
|
type: SpreadsheetColumnType.matchedSelectOptions;
|
||||||
index: number;
|
index: number;
|
||||||
header: string;
|
header: string;
|
||||||
value: string;
|
value: T;
|
||||||
matchedOptions: SpreadsheetMatchedOptions[];
|
matchedOptions: SpreadsheetMatchedOptions<T>[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SpreadsheetErrorColumn = {
|
export type SpreadsheetErrorColumn<T> = {
|
||||||
type: SpreadsheetColumnType.matchedError;
|
type: SpreadsheetColumnType.matchedError;
|
||||||
index: number;
|
index: number;
|
||||||
header: string;
|
header: string;
|
||||||
value: string;
|
value: T;
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SpreadsheetColumn =
|
export type SpreadsheetColumn<T extends string> =
|
||||||
| SpreadsheetEmptyColumn
|
| SpreadsheetEmptyColumn
|
||||||
| SpreadsheetIgnoredColumn
|
| SpreadsheetIgnoredColumn
|
||||||
| SpreadsheetMatchedColumn
|
| SpreadsheetMatchedColumn<T>
|
||||||
| SpreadsheetMatchedSwitchColumn
|
| SpreadsheetMatchedSwitchColumn<T>
|
||||||
| SpreadsheetMatchedSelectColumn
|
| SpreadsheetMatchedSelectColumn<T>
|
||||||
| SpreadsheetMatchedSelectOptionsColumn
|
| SpreadsheetMatchedSelectOptionsColumn<T>
|
||||||
| SpreadsheetErrorColumn;
|
| SpreadsheetErrorColumn<T>;
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||||
|
|
||||||
export type SpreadsheetColumns = SpreadsheetColumn[];
|
export type SpreadsheetColumns<T extends string> = SpreadsheetColumn<T>[];
|
||||||
|
|||||||
@ -8,11 +8,11 @@ import { SpreadsheetImportRowHook } from '@/spreadsheet-import/types/Spreadsheet
|
|||||||
import { SpreadsheetImportTableHook } from '@/spreadsheet-import/types/SpreadsheetImportTableHook';
|
import { SpreadsheetImportTableHook } from '@/spreadsheet-import/types/SpreadsheetImportTableHook';
|
||||||
import { SpreadsheetImportStep } from '../steps/types/SpreadsheetImportStep';
|
import { SpreadsheetImportStep } from '../steps/types/SpreadsheetImportStep';
|
||||||
|
|
||||||
export type SpreadsheetImportDialogOptions = {
|
export type SpreadsheetImportDialogOptions<FieldNames extends string> = {
|
||||||
// callback when RSI is closed before final submit
|
// callback when RSI is closed before final submit
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
// Field description for requested data
|
// Field description for requested data
|
||||||
spreadsheetImportFields: SpreadsheetImportFields;
|
fields: SpreadsheetImportFields<FieldNames>;
|
||||||
// Runs after file upload step, receives and returns raw sheet data
|
// Runs after file upload step, receives and returns raw sheet data
|
||||||
uploadStepHook?: (importedRows: ImportedRow[]) => Promise<ImportedRow[]>;
|
uploadStepHook?: (importedRows: ImportedRow[]) => Promise<ImportedRow[]>;
|
||||||
// Runs after header selection step, receives and returns raw sheet data
|
// Runs after header selection step, receives and returns raw sheet data
|
||||||
@ -22,17 +22,17 @@ export type SpreadsheetImportDialogOptions = {
|
|||||||
) => Promise<{ headerRow: ImportedRow; importedRows: ImportedRow[] }>;
|
) => Promise<{ headerRow: ImportedRow; importedRows: ImportedRow[] }>;
|
||||||
// Runs once before validation step, used for data mutations and if you want to change how columns were matched
|
// Runs once before validation step, used for data mutations and if you want to change how columns were matched
|
||||||
matchColumnsStepHook?: (
|
matchColumnsStepHook?: (
|
||||||
importedStructuredRows: ImportedStructuredRow[],
|
importedStructuredRows: ImportedStructuredRow<FieldNames>[],
|
||||||
importedRows: ImportedRow[],
|
importedRows: ImportedRow[],
|
||||||
columns: SpreadsheetColumns,
|
columns: SpreadsheetColumns<FieldNames>,
|
||||||
) => Promise<ImportedStructuredRow[]>;
|
) => Promise<ImportedStructuredRow<FieldNames>[]>;
|
||||||
// Runs after column matching and on entry change
|
// Runs after column matching and on entry change
|
||||||
rowHook?: SpreadsheetImportRowHook;
|
rowHook?: SpreadsheetImportRowHook<FieldNames>;
|
||||||
// Runs after column matching and on entry change
|
// Runs after column matching and on entry change
|
||||||
tableHook?: SpreadsheetImportTableHook;
|
tableHook?: SpreadsheetImportTableHook<FieldNames>;
|
||||||
// Function called after user finishes the flow
|
// Function called after user finishes the flow
|
||||||
onSubmit: (
|
onSubmit: (
|
||||||
validationResult: SpreadsheetImportImportValidationResult,
|
validationResult: SpreadsheetImportImportValidationResult<FieldNames>,
|
||||||
file: File,
|
file: File,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
// Function called when user aborts the importing flow
|
// Function called when user aborts the importing flow
|
||||||
@ -59,6 +59,5 @@ export type SpreadsheetImportDialogOptions = {
|
|||||||
rtl?: boolean;
|
rtl?: boolean;
|
||||||
// Allow header selection
|
// Allow header selection
|
||||||
selectHeader?: boolean;
|
selectHeader?: boolean;
|
||||||
// Available field for import
|
|
||||||
availableFieldMetadataItems: FieldMetadataItem[];
|
availableFieldMetadataItems: FieldMetadataItem[];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,34 +1,25 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
|
||||||
import { SpreadsheetImportFieldType } from '@/spreadsheet-import/types/SpreadsheetImportFieldType';
|
import { SpreadsheetImportFieldType } from '@/spreadsheet-import/types/SpreadsheetImportFieldType';
|
||||||
import { SpreadsheetImportFieldValidationDefinition } from '@/spreadsheet-import/types/SpreadsheetImportFieldValidationDefinition';
|
import { SpreadsheetImportFieldValidationDefinition } from '@/spreadsheet-import/types/SpreadsheetImportFieldValidationDefinition';
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
import { IconComponent } from 'twenty-ui/display';
|
import { IconComponent } from 'twenty-ui/display';
|
||||||
|
|
||||||
export type SpreadsheetImportField = {
|
export type SpreadsheetImportField<T extends string> = {
|
||||||
// Icon
|
// Icon
|
||||||
Icon: IconComponent | null | undefined;
|
Icon: IconComponent | null | undefined;
|
||||||
// UI-facing field label
|
// UI-facing field label
|
||||||
label: string;
|
label: string;
|
||||||
// Field's unique identifier
|
// Field's unique identifier
|
||||||
key: string;
|
key: T;
|
||||||
// Field's metadata item id - same for all associated nested fields
|
|
||||||
fieldMetadataItemId: string;
|
|
||||||
// UI-facing additional information displayed via tooltip and ? icon
|
// UI-facing additional information displayed via tooltip and ? icon
|
||||||
description?: string;
|
description?: string;
|
||||||
|
// Alternate labels used for fields' auto-matching, e.g. "fname" -> "firstName"
|
||||||
|
alternateMatches?: string[];
|
||||||
// Validations used for field entries
|
// Validations used for field entries
|
||||||
fieldValidationDefinitions?: SpreadsheetImportFieldValidationDefinition[];
|
fieldValidationDefinitions?: SpreadsheetImportFieldValidationDefinition[];
|
||||||
// Field entry component, default: Input
|
// Field entry component, default: Input
|
||||||
fieldType: SpreadsheetImportFieldType;
|
fieldType: SpreadsheetImportFieldType;
|
||||||
// Field metadata type
|
// Field metadata type
|
||||||
fieldMetadataType: FieldMetadataType;
|
fieldMetadataType: FieldMetadataType;
|
||||||
// if true, it can be a composite sub-field or a relation connect field (or both)
|
// UI-facing values shown to user as field examples pre-upload phase
|
||||||
isNestedField: boolean;
|
example?: string;
|
||||||
// can be true only if isNestedField is true
|
|
||||||
isCompositeSubField?: boolean;
|
|
||||||
// defined only if isCompositeSubField is true
|
|
||||||
compositeSubFieldKey?: string;
|
|
||||||
// can be true only if isNestedField is true
|
|
||||||
isRelationConnectField?: boolean;
|
|
||||||
// defined only if isRelationConnectField is true
|
|
||||||
uniqueFieldMetadataItem?: FieldMetadataItem;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
import { IconComponent } from 'twenty-ui/display';
|
|
||||||
|
|
||||||
export type SpreadsheetImportFieldOption = {
|
|
||||||
Icon: IconComponent | null | undefined;
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
shortLabelForNestedField?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
fieldMetadataTypeLabel?: string;
|
|
||||||
isNestedField?: boolean;
|
|
||||||
fieldMetadataItemId?: string;
|
|
||||||
};
|
|
||||||
@ -1,4 +1,6 @@
|
|||||||
import { SpreadsheetImportField } from '@/spreadsheet-import/types/SpreadsheetImportField';
|
import { SpreadsheetImportField } from '@/spreadsheet-import/types/SpreadsheetImportField';
|
||||||
import { ReadonlyDeep } from 'type-fest';
|
import { ReadonlyDeep } from 'type-fest';
|
||||||
|
|
||||||
export type SpreadsheetImportFields = ReadonlyDeep<SpreadsheetImportField[]>;
|
export type SpreadsheetImportFields<T extends string> = ReadonlyDeep<
|
||||||
|
SpreadsheetImportField<T>[]
|
||||||
|
>;
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { ImportedStructuredRowMetadata } from '@/spreadsheet-import/steps/components/ValidationStep/types';
|
import { ImportedStructuredRowMetadata } from '@/spreadsheet-import/steps/components/ValidationStep/types';
|
||||||
import { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow';
|
import { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow';
|
||||||
|
|
||||||
export type SpreadsheetImportImportValidationResult = {
|
export type SpreadsheetImportImportValidationResult<T extends string> = {
|
||||||
validStructuredRows: ImportedStructuredRow[];
|
validStructuredRows: ImportedStructuredRow<T>[];
|
||||||
invalidStructuredRows: ImportedStructuredRow[];
|
invalidStructuredRows: ImportedStructuredRow<T>[];
|
||||||
allStructuredRows: (ImportedStructuredRow & ImportedStructuredRowMetadata)[];
|
allStructuredRows: (ImportedStructuredRow<T> &
|
||||||
|
ImportedStructuredRowMetadata)[];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
export type ImportedStructuredRow = {
|
export type ImportedStructuredRow<T extends string> = {
|
||||||
[key: string]: string | boolean | undefined;
|
[key in T]: string | boolean | undefined;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow';
|
import { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow';
|
||||||
import { SpreadsheetImportInfo } from './SpreadsheetImportInfo';
|
import { SpreadsheetImportInfo } from './SpreadsheetImportInfo';
|
||||||
|
|
||||||
export type SpreadsheetImportRowHook = (
|
export type SpreadsheetImportRowHook<T extends string> = (
|
||||||
row: ImportedStructuredRow,
|
row: ImportedStructuredRow<T>,
|
||||||
addError: (fieldKey: string, error: SpreadsheetImportInfo) => void,
|
addError: (fieldKey: T, error: SpreadsheetImportInfo) => void,
|
||||||
table: ImportedStructuredRow[],
|
table: ImportedStructuredRow<T>[],
|
||||||
) => ImportedStructuredRow;
|
) => ImportedStructuredRow<T>;
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow';
|
import { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow';
|
||||||
import { SpreadsheetImportInfo } from './SpreadsheetImportInfo';
|
import { SpreadsheetImportInfo } from './SpreadsheetImportInfo';
|
||||||
|
|
||||||
export type SpreadsheetImportTableHook = (
|
export type SpreadsheetImportTableHook<T extends string> = (
|
||||||
table: ImportedStructuredRow[],
|
table: ImportedStructuredRow<T>[],
|
||||||
addError: (
|
addError: (
|
||||||
rowIndex: number,
|
rowIndex: number,
|
||||||
fieldKey: string,
|
fieldKey: T,
|
||||||
error: SpreadsheetImportInfo,
|
error: SpreadsheetImportInfo,
|
||||||
) => void,
|
) => void,
|
||||||
) => ImportedStructuredRow[];
|
) => ImportedStructuredRow<T>[];
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export type SpreadsheetMatchedOptions = {
|
export type SpreadsheetMatchedOptions<T> = {
|
||||||
entry: string;
|
entry: string;
|
||||||
value?: string;
|
value?: T;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,16 +9,17 @@ import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations';
|
|||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
describe('addErrorsAndRunHooks', () => {
|
describe('addErrorsAndRunHooks', () => {
|
||||||
const requiredField = {
|
type FullData = ImportedStructuredRow<'name' | 'age' | 'country'>;
|
||||||
|
const requiredField: SpreadsheetImportField<'name'> = {
|
||||||
key: 'name',
|
key: 'name',
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
fieldValidationDefinitions: [{ rule: 'required' }],
|
fieldValidationDefinitions: [{ rule: 'required' }],
|
||||||
Icon: null,
|
Icon: null,
|
||||||
fieldType: { type: 'input' },
|
fieldType: { type: 'input' },
|
||||||
fieldMetadataType: FieldMetadataType.TEXT,
|
fieldMetadataType: FieldMetadataType.TEXT,
|
||||||
} as SpreadsheetImportField;
|
};
|
||||||
|
|
||||||
const regexField = {
|
const regexField: SpreadsheetImportField<'age'> = {
|
||||||
key: 'age',
|
key: 'age',
|
||||||
label: 'Age',
|
label: 'Age',
|
||||||
fieldValidationDefinitions: [
|
fieldValidationDefinitions: [
|
||||||
@ -27,20 +28,18 @@ describe('addErrorsAndRunHooks', () => {
|
|||||||
Icon: null,
|
Icon: null,
|
||||||
fieldType: { type: 'input' },
|
fieldType: { type: 'input' },
|
||||||
fieldMetadataType: FieldMetadataType.NUMBER,
|
fieldMetadataType: FieldMetadataType.NUMBER,
|
||||||
} as SpreadsheetImportField;
|
};
|
||||||
|
|
||||||
const uniqueField = {
|
const uniqueField: SpreadsheetImportField<'country'> = {
|
||||||
key: 'country',
|
key: 'country',
|
||||||
label: 'Country',
|
label: 'Country',
|
||||||
fieldValidationDefinitions: [{ rule: 'unique' }],
|
fieldValidationDefinitions: [{ rule: 'unique' }],
|
||||||
Icon: null,
|
Icon: null,
|
||||||
fieldType: { type: 'input' },
|
fieldType: { type: 'input' },
|
||||||
fieldMetadataType: FieldMetadataType.SELECT,
|
fieldMetadataType: FieldMetadataType.SELECT,
|
||||||
fieldMetadataItemId: '2',
|
};
|
||||||
isNestedField: false,
|
|
||||||
} as SpreadsheetImportField;
|
|
||||||
|
|
||||||
const functionValidationFieldTrue = {
|
const functionValidationFieldTrue: SpreadsheetImportField<'email'> = {
|
||||||
key: 'email',
|
key: 'email',
|
||||||
label: 'Email',
|
label: 'Email',
|
||||||
fieldValidationDefinitions: [
|
fieldValidationDefinitions: [
|
||||||
@ -53,11 +52,9 @@ describe('addErrorsAndRunHooks', () => {
|
|||||||
Icon: null,
|
Icon: null,
|
||||||
fieldType: { type: 'input' },
|
fieldType: { type: 'input' },
|
||||||
fieldMetadataType: FieldMetadataType.EMAILS,
|
fieldMetadataType: FieldMetadataType.EMAILS,
|
||||||
fieldMetadataItemId: '1',
|
};
|
||||||
isNestedField: false,
|
|
||||||
} as SpreadsheetImportField;
|
|
||||||
|
|
||||||
const functionValidationFieldFalse = {
|
const functionValidationFieldFalse: SpreadsheetImportField<'email'> = {
|
||||||
key: 'email',
|
key: 'email',
|
||||||
label: 'Email',
|
label: 'Email',
|
||||||
fieldValidationDefinitions: [
|
fieldValidationDefinitions: [
|
||||||
@ -70,25 +67,23 @@ describe('addErrorsAndRunHooks', () => {
|
|||||||
Icon: null,
|
Icon: null,
|
||||||
fieldType: { type: 'input' },
|
fieldType: { type: 'input' },
|
||||||
fieldMetadataType: FieldMetadataType.EMAILS,
|
fieldMetadataType: FieldMetadataType.EMAILS,
|
||||||
fieldMetadataItemId: '3',
|
};
|
||||||
isNestedField: false,
|
|
||||||
} as SpreadsheetImportField;
|
|
||||||
|
|
||||||
const validData: ImportedStructuredRow = {
|
const validData: ImportedStructuredRow<'name' | 'age'> = {
|
||||||
name: 'John',
|
name: 'John',
|
||||||
age: '30',
|
age: '30',
|
||||||
};
|
};
|
||||||
const dataWithoutNameAndInvalidAge: ImportedStructuredRow = {
|
const dataWithoutNameAndInvalidAge: ImportedStructuredRow<'name' | 'age'> = {
|
||||||
name: '',
|
name: '',
|
||||||
age: 'Invalid',
|
age: 'Invalid',
|
||||||
};
|
};
|
||||||
const dataWithDuplicatedValue: ImportedStructuredRow = {
|
const dataWithDuplicatedValue: FullData = {
|
||||||
name: 'Alice',
|
name: 'Alice',
|
||||||
age: '40',
|
age: '40',
|
||||||
country: 'Brazil',
|
country: 'Brazil',
|
||||||
};
|
};
|
||||||
|
|
||||||
const data: ImportedStructuredRow[] = [
|
const data: ImportedStructuredRow<'name' | 'age'>[] = [
|
||||||
validData,
|
validData,
|
||||||
dataWithoutNameAndInvalidAge,
|
dataWithoutNameAndInvalidAge,
|
||||||
];
|
];
|
||||||
@ -118,14 +113,18 @@ describe('addErrorsAndRunHooks', () => {
|
|||||||
level: 'error',
|
level: 'error',
|
||||||
};
|
};
|
||||||
|
|
||||||
const rowHook: SpreadsheetImportRowHook = jest.fn((row, addError) => {
|
const rowHook: SpreadsheetImportRowHook<'name' | 'age'> = jest.fn(
|
||||||
addError('name', nameError);
|
(row, addError) => {
|
||||||
return row;
|
addError('name', nameError);
|
||||||
});
|
return row;
|
||||||
const tableHook: SpreadsheetImportTableHook = jest.fn((table, addError) => {
|
},
|
||||||
addError(0, 'age', ageError);
|
);
|
||||||
return table;
|
const tableHook: SpreadsheetImportTableHook<'name' | 'age'> = jest.fn(
|
||||||
});
|
(table, addError) => {
|
||||||
|
addError(0, 'age', ageError);
|
||||||
|
return table;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it('should correctly call rowHook and tableHook and add errors', () => {
|
it('should correctly call rowHook and tableHook and add errors', () => {
|
||||||
const result = addErrorsAndRunHooks(
|
const result = addErrorsAndRunHooks(
|
||||||
@ -180,7 +179,7 @@ describe('addErrorsAndRunHooks', () => {
|
|||||||
[
|
[
|
||||||
dataWithDuplicatedValue,
|
dataWithDuplicatedValue,
|
||||||
dataWithDuplicatedValue,
|
dataWithDuplicatedValue,
|
||||||
] as unknown as ImportedStructuredRow[],
|
] as unknown as FullData[],
|
||||||
[uniqueField],
|
[uniqueField],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,93 @@
|
|||||||
|
import { SpreadsheetImportField } from '@/spreadsheet-import/types';
|
||||||
|
import { findMatch } from '@/spreadsheet-import/utils/findMatch';
|
||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
describe('findMatch', () => {
|
||||||
|
const defaultField: SpreadsheetImportField<'defaultField'> = {
|
||||||
|
key: 'defaultField',
|
||||||
|
Icon: null,
|
||||||
|
label: 'label',
|
||||||
|
fieldType: {
|
||||||
|
type: 'input',
|
||||||
|
},
|
||||||
|
fieldMetadataType: FieldMetadataType.TEXT,
|
||||||
|
alternateMatches: ['Full Name', 'First Name'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const secondaryField: SpreadsheetImportField<'secondaryField'> = {
|
||||||
|
key: 'secondaryField',
|
||||||
|
Icon: null,
|
||||||
|
label: 'label',
|
||||||
|
fieldType: {
|
||||||
|
type: 'input',
|
||||||
|
},
|
||||||
|
fieldMetadataType: FieldMetadataType.TEXT,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fields = [defaultField, secondaryField];
|
||||||
|
|
||||||
|
it('should return the matching field if the header matches exactly with the key', () => {
|
||||||
|
const autoMapDistance = 0;
|
||||||
|
|
||||||
|
const result = findMatch(defaultField.key, fields, autoMapDistance);
|
||||||
|
|
||||||
|
expect(result).toBe(defaultField.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the matching field if the header matches exactly one of the alternate matches', () => {
|
||||||
|
const autoMapDistance = 0;
|
||||||
|
|
||||||
|
const result = findMatch(
|
||||||
|
defaultField.alternateMatches?.[0] ?? '',
|
||||||
|
fields,
|
||||||
|
autoMapDistance,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(defaultField.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the matching field if the header matches partially one of the alternate matches', () => {
|
||||||
|
const header = 'First';
|
||||||
|
const autoMapDistance = 5;
|
||||||
|
|
||||||
|
const result = findMatch(header, fields, autoMapDistance);
|
||||||
|
|
||||||
|
expect(result).toBe(defaultField.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the matching field if the header matches partially both of the alternate matches', () => {
|
||||||
|
const header = 'Name';
|
||||||
|
const autoMapDistance = 5;
|
||||||
|
|
||||||
|
const result = findMatch(header, fields, autoMapDistance);
|
||||||
|
|
||||||
|
expect(result).toBe(defaultField.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined if no exact match or alternate match is found within the auto map distance', () => {
|
||||||
|
const header = 'Header';
|
||||||
|
const autoMapDistance = 2;
|
||||||
|
|
||||||
|
const result = findMatch(header, fields, autoMapDistance);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the matching field with the smallest Levenshtein distance if within auto map distance', () => {
|
||||||
|
const header = 'Name'.split('').reverse().join('');
|
||||||
|
const autoMapDistance = 100;
|
||||||
|
|
||||||
|
const result = findMatch(header, fields, autoMapDistance);
|
||||||
|
|
||||||
|
expect(result).toBe(defaultField.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined if no match is found within auto map distance', () => {
|
||||||
|
const header = 'Name'.split('').reverse().join('');
|
||||||
|
const autoMapDistance = 1;
|
||||||
|
|
||||||
|
const result = findMatch(header, fields, autoMapDistance);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -7,7 +7,7 @@ import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetCol
|
|||||||
import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields';
|
import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields';
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
const nameField: SpreadsheetImportField = {
|
const nameField: SpreadsheetImportField<'Name'> = {
|
||||||
key: 'Name',
|
key: 'Name',
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
Icon: null,
|
Icon: null,
|
||||||
@ -15,11 +15,9 @@ const nameField: SpreadsheetImportField = {
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
},
|
},
|
||||||
fieldMetadataType: FieldMetadataType.TEXT,
|
fieldMetadataType: FieldMetadataType.TEXT,
|
||||||
fieldMetadataItemId: '1',
|
|
||||||
isNestedField: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ageField: SpreadsheetImportField = {
|
const ageField: SpreadsheetImportField<'Age'> = {
|
||||||
key: 'Age',
|
key: 'Age',
|
||||||
label: 'Age',
|
label: 'Age',
|
||||||
Icon: null,
|
Icon: null,
|
||||||
@ -27,37 +25,37 @@ const ageField: SpreadsheetImportField = {
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
},
|
},
|
||||||
fieldMetadataType: FieldMetadataType.NUMBER,
|
fieldMetadataType: FieldMetadataType.NUMBER,
|
||||||
fieldMetadataItemId: '2',
|
|
||||||
isNestedField: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const validations: SpreadsheetImportFieldValidationDefinition[] = [
|
const validations: SpreadsheetImportFieldValidationDefinition[] = [
|
||||||
{ rule: 'required' },
|
{ rule: 'required' },
|
||||||
];
|
];
|
||||||
const nameFieldWithValidations: SpreadsheetImportField = {
|
const nameFieldWithValidations: SpreadsheetImportField<'Name'> = {
|
||||||
...nameField,
|
...nameField,
|
||||||
fieldValidationDefinitions: validations,
|
fieldValidationDefinitions: validations,
|
||||||
};
|
};
|
||||||
const ageFieldWithValidations: SpreadsheetImportField = {
|
const ageFieldWithValidations: SpreadsheetImportField<'Age'> = {
|
||||||
...ageField,
|
...ageField,
|
||||||
fieldValidationDefinitions: validations,
|
fieldValidationDefinitions: validations,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nameColumn: SpreadsheetColumn = {
|
type ColumnValues = 'Name' | 'Age';
|
||||||
|
|
||||||
|
const nameColumn: SpreadsheetColumn<ColumnValues> = {
|
||||||
type: SpreadsheetColumnType.matched,
|
type: SpreadsheetColumnType.matched,
|
||||||
index: 0,
|
index: 0,
|
||||||
header: '',
|
header: '',
|
||||||
value: 'Name',
|
value: 'Name',
|
||||||
};
|
};
|
||||||
|
|
||||||
const ageColumn: SpreadsheetColumn = {
|
const ageColumn: SpreadsheetColumn<ColumnValues> = {
|
||||||
type: SpreadsheetColumnType.matched,
|
type: SpreadsheetColumnType.matched,
|
||||||
index: 0,
|
index: 0,
|
||||||
header: '',
|
header: '',
|
||||||
value: 'Age',
|
value: 'Age',
|
||||||
};
|
};
|
||||||
|
|
||||||
const extraColumn: SpreadsheetColumn = {
|
const extraColumn: SpreadsheetColumn<ColumnValues> = {
|
||||||
type: SpreadsheetColumnType.matched,
|
type: SpreadsheetColumnType.matched,
|
||||||
index: 0,
|
index: 0,
|
||||||
header: '',
|
header: '',
|
||||||
|
|||||||
@ -17,7 +17,7 @@ describe('getFieldOptions', () => {
|
|||||||
value: 'Three',
|
value: 'Three',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const fields: SpreadsheetImportField[] = [
|
const fields: SpreadsheetImportField<'Options' | 'Name'>[] = [
|
||||||
{
|
{
|
||||||
key: 'Options',
|
key: 'Options',
|
||||||
Icon: null,
|
Icon: null,
|
||||||
@ -27,8 +27,6 @@ describe('getFieldOptions', () => {
|
|||||||
options: optionsArray,
|
options: optionsArray,
|
||||||
},
|
},
|
||||||
fieldMetadataType: FieldMetadataType.SELECT,
|
fieldMetadataType: FieldMetadataType.SELECT,
|
||||||
fieldMetadataItemId: '1',
|
|
||||||
isNestedField: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'Name',
|
key: 'Name',
|
||||||
@ -38,8 +36,6 @@ describe('getFieldOptions', () => {
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
},
|
},
|
||||||
fieldMetadataType: FieldMetadataType.TEXT,
|
fieldMetadataType: FieldMetadataType.TEXT,
|
||||||
fieldMetadataItemId: '2',
|
|
||||||
isNestedField: false,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,166 @@
|
|||||||
|
import { SpreadsheetImportField } from '@/spreadsheet-import/types';
|
||||||
|
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||||
|
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
||||||
|
import { getMatchedColumns } from '@/spreadsheet-import/utils/getMatchedColumns';
|
||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
describe('getMatchedColumns', () => {
|
||||||
|
const columns: SpreadsheetColumn<string>[] = [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
header: 'Name',
|
||||||
|
type: SpreadsheetColumnType.matched,
|
||||||
|
value: 'Name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
header: 'Location',
|
||||||
|
type: SpreadsheetColumnType.matched,
|
||||||
|
value: 'Location',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 2,
|
||||||
|
header: 'Age',
|
||||||
|
type: SpreadsheetColumnType.matched,
|
||||||
|
value: 'Age',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const fields: SpreadsheetImportField<string>[] = [
|
||||||
|
{
|
||||||
|
key: 'Name',
|
||||||
|
label: 'Name',
|
||||||
|
fieldType: { type: 'input' },
|
||||||
|
fieldMetadataType: FieldMetadataType.TEXT,
|
||||||
|
Icon: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Location',
|
||||||
|
label: 'Location',
|
||||||
|
fieldType: { type: 'select', options: [] },
|
||||||
|
fieldMetadataType: FieldMetadataType.POSITION,
|
||||||
|
Icon: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Age',
|
||||||
|
label: 'Age',
|
||||||
|
fieldType: { type: 'input' },
|
||||||
|
fieldMetadataType: FieldMetadataType.NUMBER,
|
||||||
|
Icon: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
['John', 'New York'],
|
||||||
|
['Alice', 'Los Angeles'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const autoMapDistance = 2;
|
||||||
|
|
||||||
|
it('should return matched columns for each field', () => {
|
||||||
|
const result = getMatchedColumns(columns, fields, data, autoMapDistance);
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
header: 'Name',
|
||||||
|
type: SpreadsheetColumnType.matched,
|
||||||
|
value: 'Name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
header: 'Location',
|
||||||
|
type: SpreadsheetColumnType.matchedSelect,
|
||||||
|
value: 'Location',
|
||||||
|
matchedOptions: [
|
||||||
|
{
|
||||||
|
entry: 'New York',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: 'Los Angeles',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 2,
|
||||||
|
header: 'Age',
|
||||||
|
type: SpreadsheetColumnType.matched,
|
||||||
|
value: 'Age',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle columns with duplicate values by choosing the closest match', () => {
|
||||||
|
const columnsWithDuplicates: SpreadsheetColumn<string>[] = [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
header: 'Name',
|
||||||
|
type: SpreadsheetColumnType.matched,
|
||||||
|
value: 'Name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
header: 'Name',
|
||||||
|
type: SpreadsheetColumnType.matched,
|
||||||
|
value: 'Name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 2,
|
||||||
|
header: 'Location',
|
||||||
|
type: SpreadsheetColumnType.matched,
|
||||||
|
value: 'Location',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = getMatchedColumns(
|
||||||
|
columnsWithDuplicates,
|
||||||
|
fields,
|
||||||
|
data,
|
||||||
|
autoMapDistance,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result[0]).toEqual({
|
||||||
|
index: 0,
|
||||||
|
header: 'Name',
|
||||||
|
type: SpreadsheetColumnType.empty,
|
||||||
|
});
|
||||||
|
expect(result[1]).toEqual({
|
||||||
|
index: 1,
|
||||||
|
header: 'Name',
|
||||||
|
type: SpreadsheetColumnType.matched,
|
||||||
|
value: 'Name',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return initial columns when no auto match is found', () => {
|
||||||
|
const unmatchedColumnsData: string[][] = [
|
||||||
|
['John', 'New York', '30'],
|
||||||
|
['Alice', 'Los Angeles', '25'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const unmatchedFields: SpreadsheetImportField<string>[] = [
|
||||||
|
{
|
||||||
|
key: 'Hobby',
|
||||||
|
label: 'Hobby',
|
||||||
|
fieldType: { type: 'input' },
|
||||||
|
fieldMetadataType: FieldMetadataType.TEXT,
|
||||||
|
Icon: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Interest',
|
||||||
|
label: 'Interest',
|
||||||
|
fieldType: { type: 'input' },
|
||||||
|
fieldMetadataType: FieldMetadataType.TEXT,
|
||||||
|
Icon: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = getMatchedColumns(
|
||||||
|
columns,
|
||||||
|
unmatchedFields,
|
||||||
|
unmatchedColumnsData,
|
||||||
|
autoMapDistance,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(columns);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -6,7 +6,7 @@ import { normalizeTableData } from '@/spreadsheet-import/utils/normalizeTableDat
|
|||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
describe('normalizeTableData', () => {
|
describe('normalizeTableData', () => {
|
||||||
const columns: SpreadsheetColumn[] = [
|
const columns: SpreadsheetColumn<string>[] = [
|
||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
@ -27,7 +27,7 @@ describe('normalizeTableData', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const fields = [
|
const fields: SpreadsheetImportField<string>[] = [
|
||||||
{
|
{
|
||||||
key: 'name',
|
key: 'name',
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
@ -51,7 +51,7 @@ describe('normalizeTableData', () => {
|
|||||||
fieldMetadataType: FieldMetadataType.BOOLEAN,
|
fieldMetadataType: FieldMetadataType.BOOLEAN,
|
||||||
Icon: null,
|
Icon: null,
|
||||||
},
|
},
|
||||||
] as SpreadsheetImportField[];
|
];
|
||||||
|
|
||||||
const rawData = [
|
const rawData = [
|
||||||
['John', '30', 'Yes'],
|
['John', '30', 'Yes'],
|
||||||
@ -70,7 +70,7 @@ describe('normalizeTableData', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should normalize matchedCheckbox values and handle booleanMatches', () => {
|
it('should normalize matchedCheckbox values and handle booleanMatches', () => {
|
||||||
const columns: SpreadsheetColumn[] = [
|
const columns: SpreadsheetColumn<string>[] = [
|
||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
header: 'Active',
|
header: 'Active',
|
||||||
@ -79,7 +79,7 @@ describe('normalizeTableData', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const fields = [
|
const fields: SpreadsheetImportField<string>[] = [
|
||||||
{
|
{
|
||||||
key: 'active',
|
key: 'active',
|
||||||
label: 'Active',
|
label: 'Active',
|
||||||
@ -89,10 +89,8 @@ describe('normalizeTableData', () => {
|
|||||||
},
|
},
|
||||||
fieldMetadataType: FieldMetadataType.BOOLEAN,
|
fieldMetadataType: FieldMetadataType.BOOLEAN,
|
||||||
Icon: null,
|
Icon: null,
|
||||||
fieldMetadataItemId: '1',
|
|
||||||
isNestedField: false,
|
|
||||||
},
|
},
|
||||||
] as SpreadsheetImportField[];
|
];
|
||||||
|
|
||||||
const rawData = [['Yes'], ['No'], ['OtherValue']];
|
const rawData = [['Yes'], ['No'], ['OtherValue']];
|
||||||
|
|
||||||
@ -102,7 +100,7 @@ describe('normalizeTableData', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should map matchedSelect and matchedSelectOptions values correctly', () => {
|
it('should map matchedSelect and matchedSelectOptions values correctly', () => {
|
||||||
const columns: SpreadsheetColumn[] = [
|
const columns: SpreadsheetColumn<string>[] = [
|
||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
header: 'Number',
|
header: 'Number',
|
||||||
@ -115,7 +113,7 @@ describe('normalizeTableData', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const fields = [
|
const fields: SpreadsheetImportField<string>[] = [
|
||||||
{
|
{
|
||||||
key: 'number',
|
key: 'number',
|
||||||
label: 'Number',
|
label: 'Number',
|
||||||
@ -129,7 +127,7 @@ describe('normalizeTableData', () => {
|
|||||||
fieldMetadataType: FieldMetadataType.SELECT,
|
fieldMetadataType: FieldMetadataType.SELECT,
|
||||||
Icon: null,
|
Icon: null,
|
||||||
},
|
},
|
||||||
] as SpreadsheetImportField[];
|
];
|
||||||
|
|
||||||
const rawData = [['One'], ['Two'], ['OtherValue']];
|
const rawData = [['One'], ['Two'], ['OtherValue']];
|
||||||
|
|
||||||
@ -143,7 +141,7 @@ describe('normalizeTableData', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle empty and ignored columns', () => {
|
it('should handle empty and ignored columns', () => {
|
||||||
const columns: SpreadsheetColumn[] = [
|
const columns: SpreadsheetColumn<string>[] = [
|
||||||
{ index: 0, header: 'Empty', type: SpreadsheetColumnType.empty },
|
{ index: 0, header: 'Empty', type: SpreadsheetColumnType.empty },
|
||||||
{ index: 1, header: 'Ignored', type: SpreadsheetColumnType.ignored },
|
{ index: 1, header: 'Ignored', type: SpreadsheetColumnType.ignored },
|
||||||
];
|
];
|
||||||
@ -156,7 +154,7 @@ describe('normalizeTableData', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle unrecognized column types and return empty object', () => {
|
it('should handle unrecognized column types and return empty object', () => {
|
||||||
const columns: SpreadsheetColumns = [
|
const columns: SpreadsheetColumns<string> = [
|
||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
header: 'Unrecognized',
|
header: 'Unrecognized',
|
||||||
|
|||||||
@ -5,15 +5,15 @@ import { setColumn } from '@/spreadsheet-import/utils/setColumn';
|
|||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
describe('setColumn', () => {
|
describe('setColumn', () => {
|
||||||
const defaultField = {
|
const defaultField: SpreadsheetImportField<'Name'> = {
|
||||||
Icon: null,
|
Icon: null,
|
||||||
label: 'label',
|
label: 'label',
|
||||||
key: 'Name',
|
key: 'Name',
|
||||||
fieldType: { type: 'input' },
|
fieldType: { type: 'input' },
|
||||||
fieldMetadataType: FieldMetadataType.TEXT,
|
fieldMetadataType: FieldMetadataType.TEXT,
|
||||||
} as SpreadsheetImportField;
|
};
|
||||||
|
|
||||||
const oldColumn: SpreadsheetColumn = {
|
const oldColumn: SpreadsheetColumn<'oldValue'> = {
|
||||||
index: 0,
|
index: 0,
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
type: SpreadsheetColumnType.matched,
|
type: SpreadsheetColumnType.matched,
|
||||||
@ -27,7 +27,7 @@ describe('setColumn', () => {
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
options: [{ value: 'John' }, { value: 'Alice' }],
|
options: [{ value: 'John' }, { value: 'Alice' }],
|
||||||
},
|
},
|
||||||
} as SpreadsheetImportField;
|
} as SpreadsheetImportField<'Name'>;
|
||||||
|
|
||||||
const data = [['John'], ['Alice']];
|
const data = [['John'], ['Alice']];
|
||||||
const result = setColumn(oldColumn, field, data);
|
const result = setColumn(oldColumn, field, data);
|
||||||
@ -54,7 +54,7 @@ describe('setColumn', () => {
|
|||||||
const field = {
|
const field = {
|
||||||
...defaultField,
|
...defaultField,
|
||||||
fieldType: { type: 'checkbox' },
|
fieldType: { type: 'checkbox' },
|
||||||
} as SpreadsheetImportField;
|
} as SpreadsheetImportField<'Name'>;
|
||||||
|
|
||||||
const result = setColumn(oldColumn, field);
|
const result = setColumn(oldColumn, field);
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ describe('setColumn', () => {
|
|||||||
const field = {
|
const field = {
|
||||||
...defaultField,
|
...defaultField,
|
||||||
fieldType: { type: 'input' },
|
fieldType: { type: 'input' },
|
||||||
} as SpreadsheetImportField;
|
} as SpreadsheetImportField<'Name'>;
|
||||||
|
|
||||||
const result = setColumn(oldColumn, field);
|
const result = setColumn(oldColumn, field);
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ describe('setColumn', () => {
|
|||||||
const field = {
|
const field = {
|
||||||
...defaultField,
|
...defaultField,
|
||||||
fieldType: { type: 'unknown' },
|
fieldType: { type: 'unknown' },
|
||||||
} as unknown as SpreadsheetImportField;
|
} as unknown as SpreadsheetImportField<'Name'>;
|
||||||
|
|
||||||
const result = setColumn(oldColumn, field);
|
const result = setColumn(oldColumn, field);
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { setIgnoreColumn } from '@/spreadsheet-import/utils/setIgnoreColumn';
|
|||||||
|
|
||||||
describe('setIgnoreColumn', () => {
|
describe('setIgnoreColumn', () => {
|
||||||
it('should return a column with type "ignored"', () => {
|
it('should return a column with type "ignored"', () => {
|
||||||
const column: SpreadsheetColumn = {
|
const column: SpreadsheetColumn<'John'> = {
|
||||||
index: 0,
|
index: 0,
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
type: SpreadsheetColumnType.matched,
|
type: SpreadsheetColumnType.matched,
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { setSubColumn } from '@/spreadsheet-import/utils/setSubColumn';
|
|||||||
|
|
||||||
describe('setSubColumn', () => {
|
describe('setSubColumn', () => {
|
||||||
it('should return a matchedSelectColumn with updated matchedOptions', () => {
|
it('should return a matchedSelectColumn with updated matchedOptions', () => {
|
||||||
const oldColumn: SpreadsheetColumn = {
|
const oldColumn: SpreadsheetColumn<'John' | ''> = {
|
||||||
index: 0,
|
index: 0,
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
type: SpreadsheetColumnType.matchedSelect,
|
type: SpreadsheetColumnType.matchedSelect,
|
||||||
@ -32,7 +32,7 @@ describe('setSubColumn', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return a matchedSelectOptionsColumn with updated matchedOptions', () => {
|
it('should return a matchedSelectOptionsColumn with updated matchedOptions', () => {
|
||||||
const oldColumn: SpreadsheetColumn = {
|
const oldColumn: SpreadsheetColumn<'John' | 'Jane'> = {
|
||||||
index: 0,
|
index: 0,
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
type: SpreadsheetColumnType.matchedSelectOptions,
|
type: SpreadsheetColumnType.matchedSelectOptions,
|
||||||
|
|||||||
@ -15,17 +15,17 @@ import {
|
|||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
export const addErrorsAndRunHooks = (
|
export const addErrorsAndRunHooks = <T extends string>(
|
||||||
data: (ImportedStructuredRow & Partial<ImportedStructuredRowMetadata>)[],
|
data: (ImportedStructuredRow<T> & Partial<ImportedStructuredRowMetadata>)[],
|
||||||
fields: SpreadsheetImportFields,
|
fields: SpreadsheetImportFields<T>,
|
||||||
rowHook?: SpreadsheetImportRowHook,
|
rowHook?: SpreadsheetImportRowHook<T>,
|
||||||
tableHook?: SpreadsheetImportTableHook,
|
tableHook?: SpreadsheetImportTableHook<T>,
|
||||||
): (ImportedStructuredRow & ImportedStructuredRowMetadata)[] => {
|
): (ImportedStructuredRow<T> & ImportedStructuredRowMetadata)[] => {
|
||||||
const errors: Errors = {};
|
const errors: Errors = {};
|
||||||
|
|
||||||
const addHookError = (
|
const addHookError = (
|
||||||
rowIndex: number,
|
rowIndex: number,
|
||||||
fieldKey: string,
|
fieldKey: T,
|
||||||
error: SpreadsheetImportInfo,
|
error: SpreadsheetImportInfo,
|
||||||
) => {
|
) => {
|
||||||
errors[rowIndex] = {
|
errors[rowIndex] = {
|
||||||
@ -48,7 +48,7 @@ export const addErrorsAndRunHooks = (
|
|||||||
field.fieldValidationDefinitions?.forEach((fieldValidationDefinition) => {
|
field.fieldValidationDefinitions?.forEach((fieldValidationDefinition) => {
|
||||||
switch (fieldValidationDefinition.rule) {
|
switch (fieldValidationDefinition.rule) {
|
||||||
case 'unique': {
|
case 'unique': {
|
||||||
const values = data.map((entry) => entry[field.key]);
|
const values = data.map((entry) => entry[field.key as T]);
|
||||||
|
|
||||||
const taken = new Set(); // Set of items used at least once
|
const taken = new Set(); // Set of items used at least once
|
||||||
const duplicates = new Set(); // Set of items used multiple times
|
const duplicates = new Set(); // Set of items used multiple times
|
||||||
@ -87,9 +87,9 @@ export const addErrorsAndRunHooks = (
|
|||||||
case 'required': {
|
case 'required': {
|
||||||
data.forEach((entry, index) => {
|
data.forEach((entry, index) => {
|
||||||
if (
|
if (
|
||||||
entry[field.key] === null ||
|
entry[field.key as T] === null ||
|
||||||
entry[field.key] === undefined ||
|
entry[field.key as T] === undefined ||
|
||||||
entry[field.key] === ''
|
entry[field.key as T] === ''
|
||||||
) {
|
) {
|
||||||
errors[index] = {
|
errors[index] = {
|
||||||
...errors[index],
|
...errors[index],
|
||||||
@ -156,17 +156,14 @@ export const addErrorsAndRunHooks = (
|
|||||||
if (!('__index' in value)) {
|
if (!('__index' in value)) {
|
||||||
value.__index = v4();
|
value.__index = v4();
|
||||||
}
|
}
|
||||||
const newValue = value as ImportedStructuredRow &
|
const newValue = value as ImportedStructuredRow<T> &
|
||||||
ImportedStructuredRowMetadata;
|
ImportedStructuredRowMetadata;
|
||||||
|
|
||||||
if (isDefined(errors[index])) {
|
if (isDefined(errors[index])) {
|
||||||
return { ...newValue, __errors: errors[index] } as ImportedStructuredRow &
|
return { ...newValue, __errors: errors[index] };
|
||||||
ImportedStructuredRowMetadata;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUndefinedOrNull(errors[index]) && isDefined(value?.__errors)) {
|
if (isUndefinedOrNull(errors[index]) && isDefined(value?.__errors)) {
|
||||||
return { ...newValue, __errors: null } as ImportedStructuredRow &
|
return { ...newValue, __errors: null };
|
||||||
ImportedStructuredRowMetadata;
|
|
||||||
}
|
}
|
||||||
return newValue;
|
return newValue;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||||
|
import lavenstein from 'js-levenshtein';
|
||||||
|
|
||||||
|
type AutoMatchAccumulator<T> = {
|
||||||
|
distance: number;
|
||||||
|
value: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findMatch = <T extends string>(
|
||||||
|
header: string,
|
||||||
|
fields: SpreadsheetImportFields<T>,
|
||||||
|
autoMapDistance: number,
|
||||||
|
): T | undefined => {
|
||||||
|
const smallestValue = fields.reduce<AutoMatchAccumulator<T>>((acc, field) => {
|
||||||
|
const distance = Math.min(
|
||||||
|
...[
|
||||||
|
lavenstein(field.key, header),
|
||||||
|
...(field.alternateMatches?.map((alternate) =>
|
||||||
|
lavenstein(alternate, header),
|
||||||
|
) || []),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return distance < acc.distance || acc.distance === undefined
|
||||||
|
? ({ value: field.key, distance } as AutoMatchAccumulator<T>)
|
||||||
|
: acc;
|
||||||
|
}, {} as AutoMatchAccumulator<T>);
|
||||||
|
return smallestValue.distance <= autoMapDistance
|
||||||
|
? smallestValue.value
|
||||||
|
: undefined;
|
||||||
|
};
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||||
|
|
||||||
export const findUnmatchedRequiredFields = (
|
export const findUnmatchedRequiredFields = <T extends string>(
|
||||||
fields: SpreadsheetImportFields,
|
fields: SpreadsheetImportFields<T>,
|
||||||
columns: SpreadsheetColumns,
|
columns: SpreadsheetColumns<T>,
|
||||||
) =>
|
) =>
|
||||||
fields
|
fields
|
||||||
.filter((field) =>
|
.filter((field) =>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||||
|
|
||||||
export const getFieldOptions = (
|
export const getFieldOptions = <T extends string>(
|
||||||
fields: SpreadsheetImportFields,
|
fields: SpreadsheetImportFields<T>,
|
||||||
fieldKey: string,
|
fieldKey: string,
|
||||||
) => {
|
) => {
|
||||||
const field = fields.find(({ key }) => fieldKey === key);
|
const field = fields.find(({ key }) => fieldKey === key);
|
||||||
|
|||||||
@ -0,0 +1,52 @@
|
|||||||
|
import lavenstein from 'js-levenshtein';
|
||||||
|
|
||||||
|
import { MatchColumnsStepProps } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||||
|
|
||||||
|
import {
|
||||||
|
SpreadsheetImportField,
|
||||||
|
SpreadsheetImportFields,
|
||||||
|
} from '@/spreadsheet-import/types';
|
||||||
|
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||||
|
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { findMatch } from './findMatch';
|
||||||
|
import { setColumn } from './setColumn';
|
||||||
|
|
||||||
|
export const getMatchedColumns = <T extends string>(
|
||||||
|
columns: SpreadsheetColumns<T>,
|
||||||
|
fields: SpreadsheetImportFields<T>,
|
||||||
|
data: MatchColumnsStepProps['data'],
|
||||||
|
autoMapDistance: number,
|
||||||
|
) =>
|
||||||
|
columns.reduce<SpreadsheetColumn<T>[]>((arr, column) => {
|
||||||
|
const autoMatch = findMatch(column.header, fields, autoMapDistance);
|
||||||
|
if (isDefined(autoMatch)) {
|
||||||
|
const field = fields.find(
|
||||||
|
(field) => field.key === autoMatch,
|
||||||
|
) as SpreadsheetImportField<T>;
|
||||||
|
const duplicateIndex = arr.findIndex(
|
||||||
|
(column) => 'value' in column && column.value === field.key,
|
||||||
|
);
|
||||||
|
const duplicate = arr[duplicateIndex];
|
||||||
|
if (duplicate && 'value' in duplicate) {
|
||||||
|
return lavenstein(duplicate.value, duplicate.header) <
|
||||||
|
lavenstein(autoMatch, column.header)
|
||||||
|
? [
|
||||||
|
...arr.slice(0, duplicateIndex),
|
||||||
|
setColumn(arr[duplicateIndex], field, data),
|
||||||
|
...arr.slice(duplicateIndex + 1),
|
||||||
|
setColumn(column),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
...arr.slice(0, duplicateIndex),
|
||||||
|
setColumn(arr[duplicateIndex]),
|
||||||
|
...arr.slice(duplicateIndex + 1),
|
||||||
|
setColumn(column, field, data),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
return [...arr, setColumn(column, field, data)];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return [...arr, column];
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
@ -11,16 +11,16 @@ import { setColumn } from '@/spreadsheet-import/utils/setColumn';
|
|||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
export const getMatchedColumnsWithFuse = ({
|
export const getMatchedColumnsWithFuse = <T extends string>({
|
||||||
columns,
|
columns,
|
||||||
fields,
|
fields,
|
||||||
data,
|
data,
|
||||||
}: {
|
}: {
|
||||||
columns: SpreadsheetColumns;
|
columns: SpreadsheetColumns<T>;
|
||||||
fields: SpreadsheetImportFields;
|
fields: SpreadsheetImportFields<T>;
|
||||||
data: MatchColumnsStepProps['data'];
|
data: MatchColumnsStepProps['data'];
|
||||||
}) => {
|
}) => {
|
||||||
const matchedColumns: SpreadsheetColumn[] = [];
|
const matchedColumns: SpreadsheetColumn<T>[] = [];
|
||||||
|
|
||||||
const fieldsToSearch = new Fuse(fields, {
|
const fieldsToSearch = new Fuse(fields, {
|
||||||
keys: ['label'],
|
keys: ['label'],
|
||||||
@ -30,8 +30,8 @@ export const getMatchedColumnsWithFuse = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const suggestedFieldsByColumnHeader: Record<
|
const suggestedFieldsByColumnHeader: Record<
|
||||||
SpreadsheetColumn['header'],
|
SpreadsheetColumn<T>['header'],
|
||||||
SpreadsheetImportField[]
|
SpreadsheetImportField<T>[]
|
||||||
> = {};
|
> = {};
|
||||||
|
|
||||||
for (const column of columns) {
|
for (const column of columns) {
|
||||||
@ -58,7 +58,7 @@ export const getMatchedColumnsWithFuse = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
suggestedFieldsByColumnHeader[column.header] = fieldsThatMatch.map(
|
suggestedFieldsByColumnHeader[column.header] = fieldsThatMatch.map(
|
||||||
(match) => match.item as SpreadsheetImportField,
|
(match) => match.item as SpreadsheetImportField<T>,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isFirstMatchValid && isFieldStillUnmatched) {
|
if (isFirstMatchValid && isFieldStillUnmatched) {
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
export const getShortNestedFieldLabel = (label: string) => {
|
|
||||||
return label.split(' / ').slice(1).join(' / ');
|
|
||||||
};
|
|
||||||
@ -9,10 +9,10 @@ import { isDefined } from 'twenty-shared/utils';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { normalizeCheckboxValue } from './normalizeCheckboxValue';
|
import { normalizeCheckboxValue } from './normalizeCheckboxValue';
|
||||||
|
|
||||||
export const normalizeTableData = (
|
export const normalizeTableData = <T extends string>(
|
||||||
columns: SpreadsheetColumns,
|
columns: SpreadsheetColumns<T>,
|
||||||
data: ImportedRow[],
|
data: ImportedRow[],
|
||||||
fields: SpreadsheetImportFields,
|
fields: SpreadsheetImportFields<T>,
|
||||||
) =>
|
) =>
|
||||||
data.map((row) =>
|
data.map((row) =>
|
||||||
columns.reduce((acc, column, index) => {
|
columns.reduce((acc, column, index) => {
|
||||||
@ -101,5 +101,5 @@ export const normalizeTableData = (
|
|||||||
default:
|
default:
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
}, {} as ImportedStructuredRow),
|
}, {} as ImportedStructuredRow<T>),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -9,17 +9,17 @@ import { isDefined } from 'twenty-shared/utils';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { uniqueEntries } from './uniqueEntries';
|
import { uniqueEntries } from './uniqueEntries';
|
||||||
|
|
||||||
export const setColumn = (
|
export const setColumn = <T extends string>(
|
||||||
oldColumn: SpreadsheetColumn,
|
oldColumn: SpreadsheetColumn<T>,
|
||||||
field?: SpreadsheetImportField,
|
field?: SpreadsheetImportField<T>,
|
||||||
data?: MatchColumnsStepProps['data'],
|
data?: MatchColumnsStepProps['data'],
|
||||||
): SpreadsheetColumn => {
|
): SpreadsheetColumn<T> => {
|
||||||
if (field?.fieldType.type === 'select') {
|
if (field?.fieldType.type === 'select') {
|
||||||
const fieldOptions = field.fieldType.options;
|
const fieldOptions = field.fieldType.options;
|
||||||
const uniqueData = uniqueEntries(
|
const uniqueData = uniqueEntries(
|
||||||
data || [],
|
data || [],
|
||||||
oldColumn.index,
|
oldColumn.index,
|
||||||
) as SpreadsheetMatchedOptions[];
|
) as SpreadsheetMatchedOptions<T>[];
|
||||||
|
|
||||||
const matchedOptions = uniqueData.map((record) => {
|
const matchedOptions = uniqueData.map((record) => {
|
||||||
const value = fieldOptions.find(
|
const value = fieldOptions.find(
|
||||||
@ -28,8 +28,8 @@ export const setColumn = (
|
|||||||
fieldOption.label === record.entry,
|
fieldOption.label === record.entry,
|
||||||
)?.value;
|
)?.value;
|
||||||
return value
|
return value
|
||||||
? ({ ...record, value } as SpreadsheetMatchedOptions)
|
? ({ ...record, value } as SpreadsheetMatchedOptions<T>)
|
||||||
: (record as SpreadsheetMatchedOptions);
|
: (record as SpreadsheetMatchedOptions<T>);
|
||||||
});
|
});
|
||||||
const allMatched =
|
const allMatched =
|
||||||
matchedOptions.filter((o) => o.value).length === uniqueData?.length;
|
matchedOptions.filter((o) => o.value).length === uniqueData?.length;
|
||||||
@ -77,8 +77,8 @@ export const setColumn = (
|
|||||||
fieldOption.value === entry || fieldOption.label === entry,
|
fieldOption.value === entry || fieldOption.label === entry,
|
||||||
)?.value;
|
)?.value;
|
||||||
return value
|
return value
|
||||||
? ({ entry, value } as SpreadsheetMatchedOptions)
|
? ({ entry, value } as SpreadsheetMatchedOptions<T>)
|
||||||
: ({ entry } as SpreadsheetMatchedOptions);
|
: ({ entry } as SpreadsheetMatchedOptions<T>);
|
||||||
});
|
});
|
||||||
const areAllMatched =
|
const areAllMatched =
|
||||||
matchedOptions.filter((option) => option.value).length ===
|
matchedOptions.filter((option) => option.value).length ===
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||||
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
||||||
|
|
||||||
export const setIgnoreColumn = ({
|
export const setIgnoreColumn = <T extends string>({
|
||||||
header,
|
header,
|
||||||
index,
|
index,
|
||||||
}: SpreadsheetColumn): SpreadsheetColumn => ({
|
}: SpreadsheetColumn<T>): SpreadsheetColumn<T> => ({
|
||||||
header,
|
header,
|
||||||
index,
|
index,
|
||||||
type: SpreadsheetColumnType.ignored,
|
type: SpreadsheetColumnType.ignored,
|
||||||
|
|||||||
@ -5,13 +5,15 @@ import {
|
|||||||
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
||||||
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
|
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
|
||||||
|
|
||||||
export const setSubColumn = (
|
export const setSubColumn = <T>(
|
||||||
oldColumn:
|
oldColumn:
|
||||||
| SpreadsheetMatchedSelectColumn
|
| SpreadsheetMatchedSelectColumn<T>
|
||||||
| SpreadsheetMatchedSelectOptionsColumn,
|
| SpreadsheetMatchedSelectOptionsColumn<T>,
|
||||||
entry: string,
|
entry: string,
|
||||||
value: string,
|
value: string,
|
||||||
): SpreadsheetMatchedSelectColumn | SpreadsheetMatchedSelectOptionsColumn => {
|
):
|
||||||
|
| SpreadsheetMatchedSelectColumn<T>
|
||||||
|
| SpreadsheetMatchedSelectOptionsColumn<T> => {
|
||||||
const shouldUnselectValue =
|
const shouldUnselectValue =
|
||||||
oldColumn.matchedOptions.find((option) => option.entry === entry)?.value ===
|
oldColumn.matchedOptions.find((option) => option.entry === entry)?.value ===
|
||||||
value;
|
value;
|
||||||
@ -26,13 +28,13 @@ export const setSubColumn = (
|
|||||||
if (allMatched) {
|
if (allMatched) {
|
||||||
return {
|
return {
|
||||||
...oldColumn,
|
...oldColumn,
|
||||||
matchedOptions: options as SpreadsheetMatchedOptions[],
|
matchedOptions: options as SpreadsheetMatchedOptions<T>[],
|
||||||
type: SpreadsheetColumnType.matchedSelectOptions,
|
type: SpreadsheetColumnType.matchedSelectOptions,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
...oldColumn,
|
...oldColumn,
|
||||||
matchedOptions: options as SpreadsheetMatchedOptions[],
|
matchedOptions: options as SpreadsheetMatchedOptions<T>[],
|
||||||
type: SpreadsheetColumnType.matchedSelect,
|
type: SpreadsheetColumnType.matchedSelect,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
import { getFieldMetadataTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFieldMetadataTypeLabel';
|
||||||
|
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||||
|
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
export const spreadsheetBuildFieldOptions = <T extends string>(
|
||||||
|
fields: SpreadsheetImportFields<T>,
|
||||||
|
columns: SpreadsheetColumns<string>,
|
||||||
|
) => {
|
||||||
|
return fields
|
||||||
|
.filter((field) => field.fieldMetadataType !== FieldMetadataType.RICH_TEXT)
|
||||||
|
.map(({ Icon, label, key, fieldMetadataType }) => {
|
||||||
|
const isSelected =
|
||||||
|
columns.findIndex((column) => {
|
||||||
|
if ('value' in column) {
|
||||||
|
return column.value === key;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}) !== -1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
Icon: Icon,
|
||||||
|
value: key,
|
||||||
|
label: label,
|
||||||
|
disabled: isSelected,
|
||||||
|
fieldMetadataTypeLabel: getFieldMetadataTypeLabel(fieldMetadataType),
|
||||||
|
} as const;
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,42 +0,0 @@
|
|||||||
import { getFieldMetadataTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFieldMetadataTypeLabel';
|
|
||||||
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
|
||||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
|
||||||
import { SpreadsheetImportFieldOption } from '@/spreadsheet-import/types/SpreadsheetImportFieldOption';
|
|
||||||
import { getShortNestedFieldLabel } from '@/spreadsheet-import/utils/getShortNestedFieldLabel';
|
|
||||||
import { ReadonlyDeep } from 'type-fest';
|
|
||||||
|
|
||||||
export const spreadsheetImportBuildFieldOptions = (
|
|
||||||
fields: SpreadsheetImportFields,
|
|
||||||
columns: SpreadsheetColumns,
|
|
||||||
): readonly ReadonlyDeep<SpreadsheetImportFieldOption>[] => {
|
|
||||||
return fields.map(
|
|
||||||
({
|
|
||||||
Icon,
|
|
||||||
label,
|
|
||||||
key,
|
|
||||||
fieldMetadataType,
|
|
||||||
isNestedField,
|
|
||||||
fieldMetadataItemId,
|
|
||||||
}) => {
|
|
||||||
const isSelected = columns.some((column) => {
|
|
||||||
if ('value' in column) {
|
|
||||||
return column.value === key;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
Icon: Icon,
|
|
||||||
value: key,
|
|
||||||
label,
|
|
||||||
shortLabelForNestedField: isNestedField
|
|
||||||
? getShortNestedFieldLabel(label)
|
|
||||||
: undefined,
|
|
||||||
disabled: isSelected,
|
|
||||||
fieldMetadataTypeLabel: getFieldMetadataTypeLabel(fieldMetadataType),
|
|
||||||
isNestedField: isNestedField,
|
|
||||||
fieldMetadataItemId: fieldMetadataItemId,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
|
||||||
import { SpreadsheetImportFieldOption } from '@/spreadsheet-import/types/SpreadsheetImportFieldOption';
|
|
||||||
|
|
||||||
export const getSubFieldOptions = (
|
|
||||||
fieldMetadataItem: FieldMetadataItem,
|
|
||||||
options: readonly Readonly<SpreadsheetImportFieldOption>[],
|
|
||||||
searchFilter: string,
|
|
||||||
): readonly Readonly<SpreadsheetImportFieldOption>[] => {
|
|
||||||
return options.filter(
|
|
||||||
(option) =>
|
|
||||||
option.fieldMetadataItemId === fieldMetadataItem.id &&
|
|
||||||
option.label.toLowerCase().includes(searchFilter.toLowerCase()),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
|
||||||
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
|
||||||
import { RelationType } from '~/generated/graphql';
|
|
||||||
|
|
||||||
export const hasNestedFields = (fieldMetadata: FieldMetadataItem) => {
|
|
||||||
return (
|
|
||||||
(fieldMetadata.type === FieldMetadataType.RELATION &&
|
|
||||||
fieldMetadata.relation?.type === RelationType.MANY_TO_ONE) ||
|
|
||||||
isCompositeFieldType(fieldMetadata.type)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -3,10 +3,10 @@ import uniqBy from 'lodash.uniqby';
|
|||||||
import { MatchColumnsStepProps } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
import { MatchColumnsStepProps } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||||
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
|
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
|
||||||
|
|
||||||
export const uniqueEntries = (
|
export const uniqueEntries = <T extends string>(
|
||||||
data: MatchColumnsStepProps['data'],
|
data: MatchColumnsStepProps['data'],
|
||||||
index: number,
|
index: number,
|
||||||
): Partial<SpreadsheetMatchedOptions>[] =>
|
): Partial<SpreadsheetMatchedOptions<T>>[] =>
|
||||||
uniqBy(
|
uniqBy(
|
||||||
data.map((row) => ({ entry: row[index] })),
|
data.map((row) => ({ entry: row[index] })),
|
||||||
'entry',
|
'entry',
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import {
|
|||||||
GraphQLInputType,
|
GraphQLInputType,
|
||||||
GraphQLString,
|
GraphQLString,
|
||||||
} from 'graphql';
|
} from 'graphql';
|
||||||
import { getUniqueConstraintsFields } from 'twenty-shared/utils';
|
|
||||||
|
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||||
|
|
||||||
@ -18,6 +17,7 @@ import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-build
|
|||||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||||
|
import { getUniqueConstraintsFields } from 'src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util';
|
||||||
import { pascalCase } from 'src/utils/pascal-case';
|
import { pascalCase } from 'src/utils/pascal-case';
|
||||||
|
|
||||||
export const formatRelationConnectInputTarget = (objectMetadataId: string) =>
|
export const formatRelationConnectInputTarget = (objectMetadataId: string) =>
|
||||||
|
|||||||
@ -10,14 +10,14 @@ export const fullNameCompositeType: CompositeType = {
|
|||||||
type: FieldMetadataType.TEXT,
|
type: FieldMetadataType.TEXT,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isIncludedInUniqueConstraint: false,
|
isIncludedInUniqueConstraint: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'lastName',
|
name: 'lastName',
|
||||||
type: FieldMetadataType.TEXT,
|
type: FieldMetadataType.TEXT,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isIncludedInUniqueConstraint: false,
|
isIncludedInUniqueConstraint: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { FieldMetadataType } from '@/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
import { getUniqueConstraintsFields } from '../getUniqueConstraintsFields';
|
|
||||||
|
import { getUniqueConstraintsFields } from 'src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util';
|
||||||
|
|
||||||
describe('getUniqueConstraintsFields', () => {
|
describe('getUniqueConstraintsFields', () => {
|
||||||
const mockIdField = {
|
const mockIdField = {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { isDefined } from '@/utils/validation/isDefined';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
export const getUniqueConstraintsFields = <
|
export const getUniqueConstraintsFields = <
|
||||||
K extends {
|
K extends {
|
||||||
@ -1560,9 +1560,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
|||||||
const connectFieldName = connectQueryConfig.connectFieldName;
|
const connectFieldName = connectQueryConfig.connectFieldName;
|
||||||
|
|
||||||
throw new TwentyORMException(
|
throw new TwentyORMException(
|
||||||
`Expected 1 record to connect to ${connectFieldName}, but found ${recordToConnectTotal} with conditions: ${JSON.stringify(
|
`Expected 1 record to connect to ${connectFieldName}, but found ${recordToConnectTotal}.`,
|
||||||
connectQueryConfig.recordToConnectConditionByEntityIndex[index],
|
|
||||||
)}.`,
|
|
||||||
TwentyORMExceptionCode.CONNECT_RECORD_NOT_FOUND,
|
TwentyORMExceptionCode.CONNECT_RECORD_NOT_FOUND,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import deepEqual from 'deep-equal';
|
import deepEqual from 'deep-equal';
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
import { getUniqueConstraintsFields, isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
|
||||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||||
|
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||||
|
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||||
|
import { getUniqueConstraintsFields } from 'src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util';
|
||||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||||
import { ConnectObject } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-relation-connect.type';
|
import { ConnectObject } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-relation-connect.type';
|
||||||
@ -218,14 +219,14 @@ const hasRelationConnect = (value: unknown): value is ConnectObject => {
|
|||||||
return whereKeys.every((key) => {
|
return whereKeys.every((key) => {
|
||||||
const whereValue = where[key];
|
const whereValue = where[key];
|
||||||
|
|
||||||
if (typeof whereValue === 'string' || whereValue === null) {
|
if (typeof whereValue === 'string') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (whereValue && typeof whereValue === 'object') {
|
if (whereValue && typeof whereValue === 'object') {
|
||||||
const subObj = whereValue as Record<string, unknown>;
|
const subObj = whereValue as Record<string, unknown>;
|
||||||
|
|
||||||
return Object.values(subObj).every(
|
return Object.values(subObj).every(
|
||||||
(subValue) => typeof subValue === 'string' || subValue === null,
|
(subValue) => typeof subValue === 'string',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,11 +29,11 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)',
|
|||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
id: TEST_COMPANY_1_ID,
|
id: TEST_COMPANY_1_ID,
|
||||||
domainName: { primaryLinkUrl: 'https://company1.com' },
|
domainName: { primaryLinkUrl: 'company1.com' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: TEST_COMPANY_2_ID,
|
id: TEST_COMPANY_2_ID,
|
||||||
domainName: { primaryLinkUrl: 'https://company2.com' },
|
domainName: { primaryLinkUrl: 'company2.com' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -58,7 +58,7 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)',
|
|||||||
id: TEST_PERSON_1_ID,
|
id: TEST_PERSON_1_ID,
|
||||||
company: {
|
company: {
|
||||||
connect: {
|
connect: {
|
||||||
where: { domainName: { primaryLinkUrl: 'https://company1.com' } },
|
where: { domainName: { primaryLinkUrl: 'company1.com' } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -81,7 +81,7 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)',
|
|||||||
id: TEST_PERSON_1_ID,
|
id: TEST_PERSON_1_ID,
|
||||||
company: {
|
company: {
|
||||||
connect: {
|
connect: {
|
||||||
where: { domainName: { primaryLinkUrl: 'https://company1.com' } },
|
where: { domainName: { primaryLinkUrl: 'company1.com' } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -89,7 +89,7 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)',
|
|||||||
id: TEST_PERSON_2_ID,
|
id: TEST_PERSON_2_ID,
|
||||||
company: {
|
company: {
|
||||||
connect: {
|
connect: {
|
||||||
where: { domainName: { primaryLinkUrl: 'https://company2.com' } },
|
where: { domainName: { primaryLinkUrl: 'company2.com' } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -152,7 +152,7 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)',
|
|||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
expect(response.body.errors).toBeDefined();
|
||||||
expect(response.body.errors[0].message).toBe(
|
expect(response.body.errors[0].message).toBe(
|
||||||
'Expected 1 record to connect to company, but found 0 with conditions: [["domainNamePrimaryLinkUrl","not-existing-company"]].',
|
'Expected 1 record to connect to company, but found 0.',
|
||||||
);
|
);
|
||||||
expect(response.body.errors[0].extensions.code).toBe(
|
expect(response.body.errors[0].extensions.code).toBe(
|
||||||
ErrorCode.BAD_USER_INPUT,
|
ErrorCode.BAD_USER_INPUT,
|
||||||
|
|||||||
@ -15,7 +15,6 @@ export {
|
|||||||
sanitizeURL,
|
sanitizeURL,
|
||||||
getLogoUrlFromDomainName,
|
getLogoUrlFromDomainName,
|
||||||
} from './image/getLogoUrlFromDomainName';
|
} from './image/getLogoUrlFromDomainName';
|
||||||
export { getUniqueConstraintsFields } from './indexMetadata/getUniqueConstraintsFields';
|
|
||||||
export { parseJson } from './parseJson';
|
export { parseJson } from './parseJson';
|
||||||
export { removeUndefinedFields } from './removeUndefinedFields';
|
export { removeUndefinedFields } from './removeUndefinedFields';
|
||||||
export { getGenericOperationName } from './sentry/getGenericOperationName';
|
export { getGenericOperationName } from './sentry/getGenericOperationName';
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from './getUniqueConstraintsFields';
|
|
||||||
Reference in New Issue
Block a user