Refactor spreadsheet import (#11250)

Mostly renaming objects to avoid conflicts (it was painful because names
were too generic so you could cmd+replace easily)

Also refactoring `useBuildAvailableFieldsForImport`
This commit is contained in:
Félix Malfait
2025-03-28 07:56:51 +01:00
committed by GitHub
parent 9af2628264
commit e9e33c4d29
84 changed files with 960 additions and 916 deletions

View File

@ -1,9 +1,8 @@
import { useMemo } from 'react';
import { IconCircleOff, IconComponentProps } from 'twenty-ui';
import { IconCircleOff, IconComponentProps, SelectOption } from 'twenty-ui';
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { SelectOption } from '@/spreadsheet-import/types';
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
import { CountryCode } from 'libphonenumber-js';

View File

@ -1,9 +1,8 @@
import { useMemo } from 'react';
import { IconCircleOff, IconComponentProps } from 'twenty-ui';
import { IconCircleOff, IconComponentProps, SelectOption } from 'twenty-ui';
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { SelectOption } from '@/spreadsheet-import/types';
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
export const FormCountrySelectInput = ({

View File

@ -8,7 +8,6 @@ import { FormMultiSelectFieldInputHotKeyScope } from '@/object-record/record-fie
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-field/meta-types/input/constants/SelectFieldInputSelectableListComponentInstanceId';
import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata';
import { SelectOption } from '@/spreadsheet-import/types';
import { MultiSelectDisplay } from '@/ui/field/display/components/MultiSelectDisplay';
import { MultiSelectInput } from '@/ui/field/input/components/MultiSelectInput';
import { InputLabel } from '@/ui/input/components/InputLabel';
@ -17,8 +16,8 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { useTheme } from '@emotion/react';
import { useId, useState } from 'react';
import { IconChevronDown, VisibilityHidden } from 'twenty-ui';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronDown, SelectOption, VisibilityHidden } from 'twenty-ui';
type FormMultiSelectFieldInputProps = {
label?: string;

View File

@ -5,7 +5,6 @@ import { VariableChipStandalone } from '@/object-record/record-field/form-types/
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-field/meta-types/input/constants/SelectFieldInputSelectableListComponentInstanceId';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { SelectOption } from '@/spreadsheet-import/types';
import { SelectDisplay } from '@/ui/field/display/components/SelectDisplay';
import { SelectInput } from '@/ui/field/input/components/SelectInput';
import { InputLabel } from '@/ui/input/components/InputLabel';
@ -18,8 +17,8 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useId, useState } from 'react';
import { Key } from 'ts-key-enum';
import { IconChevronDown, VisibilityHidden } from 'twenty-ui';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronDown, SelectOption, VisibilityHidden } from 'twenty-ui';
type FormSelectFieldInputProps = {
label?: string;
@ -245,7 +244,7 @@ export const FormSelectFieldInput = ({
<SelectDisplay
color={selectedOption.color ?? 'transparent'}
label={selectedOption.label}
Icon={selectedOption.icon ?? undefined}
Icon={selectedOption.Icon ?? undefined}
preventPadding={preventDisplayPadding}
/>
</StyledSelectDisplayContainer>
@ -269,7 +268,7 @@ export const FormSelectFieldInput = ({
<SelectDisplay
color={selectedOption.color ?? 'transparent'}
label={selectedOption.label}
Icon={selectedOption.icon ?? undefined}
Icon={selectedOption.Icon ?? undefined}
preventPadding={preventDisplayPadding}
/>
</StyledSelectDisplayContainer>

View File

@ -2,14 +2,13 @@ import { useClearField } from '@/object-record/record-field/hooks/useClearField'
import { useSelectField } from '@/object-record/record-field/meta-types/hooks/useSelectField';
import { SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-field/meta-types/input/constants/SelectFieldInputSelectableListComponentInstanceId';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { SelectOption } from '@/spreadsheet-import/types';
import { SelectInput } from '@/ui/field/input/components/SelectInput';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useState } from 'react';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils';
import { SelectOption } from 'twenty-ui';
type SelectFieldInputProps = {
onSubmit?: FieldInputEvent;
onCancel?: () => void;

View File

@ -7,6 +7,11 @@ import { AvailableFieldForImport } from '@/object-record/spreadsheet-import/type
import { getSpreadSheetFieldValidationDefinitions } from '@/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions';
import { FieldMetadataType } from '~/generated-metadata/graphql';
type CompositeFieldType = keyof typeof COMPOSITE_FIELD_IMPORT_LABELS;
// Helper type for field validation type resolvers
type ValidationTypeResolver = (key: string, label: string) => FieldMetadataType;
export const useBuildAvailableFieldsForImport = () => {
const { getIcon } = useIcons();
@ -15,243 +20,160 @@ export const useBuildAvailableFieldsForImport = () => {
) => {
const availableFieldsForImport: AvailableFieldForImport[] = [];
// Todo: refactor this to avoid this else if syntax with duplicated code
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,
validationTypeResolver?: ValidationTypeResolver,
) => {
Object.entries(COMPOSITE_FIELD_IMPORT_LABELS[fieldType]).forEach(
([key, fieldLabel]) => {
const label = `${fieldLabel} (${fieldMetadataItem.label})`;
// Use the custom validation type if provided, otherwise use the field's type
const validationType = validationTypeResolver
? validationTypeResolver(key, fieldLabel)
: fieldMetadataItem.type;
availableFieldsForImport.push(
createBaseField(fieldMetadataItem, {
label,
key: `${fieldLabel} (${fieldMetadataItem.name})`,
fieldValidationDefinitions:
getSpreadSheetFieldValidationDefinitions(validationType, label),
}),
);
},
);
};
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)`,
),
}),
);
};
// Special validation type resolver for currency fields
const currencyValidationResolver: ValidationTypeResolver = (key) =>
key === 'amountMicrosLabel'
? FieldMetadataType.NUMBER
: FieldMetadataType.CURRENCY;
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,
currencyValidationResolver,
);
},
[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) {
if (fieldMetadataItem.type === FieldMetadataType.FULL_NAME) {
const { firstNameLabel, lastNameLabel } =
COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.FULL_NAME];
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: `${firstNameLabel} (${fieldMetadataItem.label})`,
key: `${firstNameLabel} (${fieldMetadataItem.name})`,
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
`${firstNameLabel} (${fieldMetadataItem.label})`,
),
});
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: `${lastNameLabel} (${fieldMetadataItem.label})`,
key: `${lastNameLabel} (${fieldMetadataItem.name})`,
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
`${lastNameLabel} (${fieldMetadataItem.label})`,
),
});
} else if (fieldMetadataItem.type === FieldMetadataType.RELATION) {
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: fieldMetadataItem.label + ' (ID)',
key: fieldMetadataItem.name,
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
fieldMetadataItem.label + ' (ID)',
),
});
} else if (fieldMetadataItem.type === FieldMetadataType.CURRENCY) {
const { currencyCodeLabel, amountMicrosLabel } =
COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.CURRENCY];
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: `${currencyCodeLabel} (${fieldMetadataItem.label})`,
key: `${currencyCodeLabel} (${fieldMetadataItem.name})`,
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
`${currencyCodeLabel} (${fieldMetadataItem.label})`,
),
});
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: `${amountMicrosLabel} (${fieldMetadataItem.label})`,
key: `${amountMicrosLabel} (${fieldMetadataItem.name})`,
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
FieldMetadataType.NUMBER,
`${amountMicrosLabel} (${fieldMetadataItem.label})`,
),
});
} else if (fieldMetadataItem.type === FieldMetadataType.ADDRESS) {
Object.entries(
COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.ADDRESS],
).forEach(([_, fieldLabel]) => {
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: `${fieldLabel} (${fieldMetadataItem.label})`,
key: `${fieldLabel} (${fieldMetadataItem.name})`,
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions:
getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
`${fieldLabel} (${fieldMetadataItem.label})`,
),
});
});
} else if (fieldMetadataItem.type === FieldMetadataType.LINKS) {
Object.entries(
COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.LINKS],
).forEach(([_, fieldLabel]) => {
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: `${fieldLabel} (${fieldMetadataItem.label})`,
key: `${fieldLabel} (${fieldMetadataItem.name})`,
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions:
getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
`${fieldLabel} (${fieldMetadataItem.label})`,
),
});
});
} else if (fieldMetadataItem.type === FieldMetadataType.SELECT) {
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: fieldMetadataItem.label,
key: fieldMetadataItem.name,
fieldType: {
type: 'select',
options:
fieldMetadataItem.options?.map((option) => ({
label: option.label,
value: option.value,
color: option.color,
})) || [],
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
fieldMetadataItem.label + ' (ID)',
),
});
} else if (fieldMetadataItem.type === FieldMetadataType.MULTI_SELECT) {
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: fieldMetadataItem.label,
key: fieldMetadataItem.name,
fieldType: {
type: 'multiSelect',
options:
fieldMetadataItem.options?.map((option) => ({
label: option.label,
value: option.value,
color: option.color,
})) || [],
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
fieldMetadataItem.label + ' (ID)',
),
});
} else if (fieldMetadataItem.type === FieldMetadataType.BOOLEAN) {
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: fieldMetadataItem.label,
key: fieldMetadataItem.name,
fieldType: {
type: 'checkbox',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
fieldMetadataItem.label,
),
});
} else if (fieldMetadataItem.type === FieldMetadataType.EMAILS) {
Object.entries(
COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.EMAILS],
).forEach(([_, fieldLabel]) => {
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: `${fieldLabel} (${fieldMetadataItem.label})`,
key: `${fieldLabel} (${fieldMetadataItem.name})`,
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions:
getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
`${fieldLabel} (${fieldMetadataItem.label})`,
),
});
});
} else if (fieldMetadataItem.type === FieldMetadataType.PHONES) {
Object.entries(
COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.PHONES],
).forEach(([_, fieldLabel]) => {
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: `${fieldLabel} (${fieldMetadataItem.label})`,
key: `${fieldLabel} (${fieldMetadataItem.name})`,
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions:
getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
`${fieldLabel} (${fieldMetadataItem.label})`,
),
});
});
} else if (fieldMetadataItem.type === FieldMetadataType.RICH_TEXT_V2) {
Object.entries(
COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.RICH_TEXT_V2],
).forEach(([_, fieldLabel]) => {
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: `${fieldLabel} (${fieldMetadataItem.label})`,
key: `${fieldLabel} (${fieldMetadataItem.name})`,
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
});
});
} else {
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: fieldMetadataItem.label,
key: fieldMetadataItem.name,
fieldType: {
type: 'input',
},
fieldMetadataType: fieldMetadataItem.type,
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
fieldMetadataItem.label,
),
});
}
const handler =
fieldTypeHandlers[fieldMetadataItem.type] || fieldTypeHandlers.default;
handler(fieldMetadataItem);
}
return availableFieldsForImport;

View File

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

View File

@ -10,11 +10,11 @@ import {
import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels';
import { ImportedStructuredRow } from '@/spreadsheet-import/types';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { z } from 'zod';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { castToString } from '~/utils/castToString';
import { convertCurrencyAmountToCurrencyMicros } from '~/utils/convertCurrencyToCurrencyMicros';
import { isDefined } from 'twenty-shared/utils';
type BuildRecordFromImportedStructuredRowArgs = {
importedStructuredRow: ImportedStructuredRow<any>;

View File

@ -1,11 +1,11 @@
import { FieldValidationDefinition } from '@/spreadsheet-import/types';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { SpreadsheetImportFieldValidationDefinition } from '@/spreadsheet-import/types';
import { absoluteUrlSchema, isDefined, isValidUuid } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const getSpreadSheetFieldValidationDefinitions = (
type: FieldMetadataType,
fieldName: string,
): FieldValidationDefinition[] => {
): SpreadsheetImportFieldValidationDefinition[] => {
switch (type) {
case FieldMetadataType.FULL_NAME:
return [