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 { 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 { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; 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 { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
import { CountryCode } from 'libphonenumber-js'; import { CountryCode } from 'libphonenumber-js';

View File

@ -1,9 +1,8 @@
import { useMemo } from 'react'; 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 { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; 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 { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
export const FormCountrySelectInput = ({ 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 { 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 { 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 { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata';
import { SelectOption } from '@/spreadsheet-import/types';
import { MultiSelectDisplay } from '@/ui/field/display/components/MultiSelectDisplay'; import { MultiSelectDisplay } from '@/ui/field/display/components/MultiSelectDisplay';
import { MultiSelectInput } from '@/ui/field/input/components/MultiSelectInput'; import { MultiSelectInput } from '@/ui/field/input/components/MultiSelectInput';
import { InputLabel } from '@/ui/input/components/InputLabel'; 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 { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { useId, useState } from 'react'; import { useId, useState } from 'react';
import { IconChevronDown, VisibilityHidden } from 'twenty-ui';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { IconChevronDown, SelectOption, VisibilityHidden } from 'twenty-ui';
type FormMultiSelectFieldInputProps = { type FormMultiSelectFieldInputProps = {
label?: string; 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 { 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 { 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 { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { SelectOption } from '@/spreadsheet-import/types';
import { SelectDisplay } from '@/ui/field/display/components/SelectDisplay'; import { SelectDisplay } from '@/ui/field/display/components/SelectDisplay';
import { SelectInput } from '@/ui/field/input/components/SelectInput'; import { SelectInput } from '@/ui/field/input/components/SelectInput';
import { InputLabel } from '@/ui/input/components/InputLabel'; import { InputLabel } from '@/ui/input/components/InputLabel';
@ -18,8 +17,8 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useId, useState } from 'react'; import { useId, useState } from 'react';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { IconChevronDown, VisibilityHidden } from 'twenty-ui';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { IconChevronDown, SelectOption, VisibilityHidden } from 'twenty-ui';
type FormSelectFieldInputProps = { type FormSelectFieldInputProps = {
label?: string; label?: string;
@ -245,7 +244,7 @@ export const FormSelectFieldInput = ({
<SelectDisplay <SelectDisplay
color={selectedOption.color ?? 'transparent'} color={selectedOption.color ?? 'transparent'}
label={selectedOption.label} label={selectedOption.label}
Icon={selectedOption.icon ?? undefined} Icon={selectedOption.Icon ?? undefined}
preventPadding={preventDisplayPadding} preventPadding={preventDisplayPadding}
/> />
</StyledSelectDisplayContainer> </StyledSelectDisplayContainer>
@ -269,7 +268,7 @@ export const FormSelectFieldInput = ({
<SelectDisplay <SelectDisplay
color={selectedOption.color ?? 'transparent'} color={selectedOption.color ?? 'transparent'}
label={selectedOption.label} label={selectedOption.label}
Icon={selectedOption.icon ?? undefined} Icon={selectedOption.Icon ?? undefined}
preventPadding={preventDisplayPadding} preventPadding={preventDisplayPadding}
/> />
</StyledSelectDisplayContainer> </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 { 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 { 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 { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { SelectOption } from '@/spreadsheet-import/types';
import { SelectInput } from '@/ui/field/input/components/SelectInput'; import { SelectInput } from '@/ui/field/input/components/SelectInput';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useState } from 'react'; import { useState } from 'react';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { SelectOption } from 'twenty-ui';
type SelectFieldInputProps = { type SelectFieldInputProps = {
onSubmit?: FieldInputEvent; onSubmit?: FieldInputEvent;
onCancel?: () => void; 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 { getSpreadSheetFieldValidationDefinitions } from '@/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions';
import { FieldMetadataType } from '~/generated-metadata/graphql'; 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 = () => { export const useBuildAvailableFieldsForImport = () => {
const { getIcon } = useIcons(); const { getIcon } = useIcons();
@ -15,243 +20,160 @@ export const useBuildAvailableFieldsForImport = () => {
) => { ) => {
const availableFieldsForImport: AvailableFieldForImport[] = []; 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) { for (const fieldMetadataItem of fieldMetadataItems) {
if (fieldMetadataItem.type === FieldMetadataType.FULL_NAME) { const handler =
const { firstNameLabel, lastNameLabel } = fieldTypeHandlers[fieldMetadataItem.type] || fieldTypeHandlers.default;
COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.FULL_NAME]; handler(fieldMetadataItem);
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,
),
});
}
} }
return availableFieldsForImport; return availableFieldsForImport;

View File

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

View File

@ -10,11 +10,11 @@ import {
import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels'; import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels';
import { ImportedStructuredRow } from '@/spreadsheet-import/types'; import { ImportedStructuredRow } from '@/spreadsheet-import/types';
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { z } from 'zod'; import { z } from 'zod';
import { FieldMetadataType } 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 { isDefined } from 'twenty-shared/utils';
type BuildRecordFromImportedStructuredRowArgs = { type BuildRecordFromImportedStructuredRowArgs = {
importedStructuredRow: ImportedStructuredRow<any>; importedStructuredRow: ImportedStructuredRow<any>;

View File

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

View File

@ -2,10 +2,10 @@
/* eslint-disable @nx/workspace-max-consts-per-file */ /* eslint-disable @nx/workspace-max-consts-per-file */
import { IANA_TIME_ZONES } from '@/localization/constants/IanaTimeZones'; import { IANA_TIME_ZONES } from '@/localization/constants/IanaTimeZones';
import { formatTimeZoneLabel } from '@/settings/accounts/utils/formatTimeZoneLabel'; import { formatTimeZoneLabel } from '@/settings/accounts/utils/formatTimeZoneLabel';
import { SelectOption } from '@/ui/input/components/Select'; import { SelectOption } from 'twenty-ui';
export const AVAILABLE_TIME_ZONE_OPTIONS_BY_LABEL = IANA_TIME_ZONES.reduce< export const AVAILABLE_TIME_ZONE_OPTIONS_BY_LABEL = IANA_TIME_ZONES.reduce<
Record<string, SelectOption<string>> Record<string, SelectOption>
>((result, ianaTimeZone) => { >((result, ianaTimeZone) => {
const timeZoneLabel = formatTimeZoneLabel(ianaTimeZone); const timeZoneLabel = formatTimeZoneLabel(ianaTimeZone);

View File

@ -4,12 +4,17 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { addressSchema as addressFieldDefaultValueSchema } from '@/object-record/record-field/types/guards/isFieldAddressValue'; import { addressSchema as addressFieldDefaultValueSchema } from '@/object-record/record-field/types/guards/isFieldAddressValue';
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect'; import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries'; import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
import { Select, SelectOption } from '@/ui/input/components/Select'; import { Select } from '@/ui/input/components/Select';
import { IconCircleOff, IconComponentProps, IconMap } from 'twenty-ui'; import { useLingui } from '@lingui/react/macro';
import {
IconCircleOff,
IconComponentProps,
IconMap,
SelectOption,
} from 'twenty-ui';
import { z } from 'zod'; import { z } from 'zod';
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString'; import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString'; import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
import { useLingui } from '@lingui/react/macro';
type SettingsDataModelFieldAddressFormProps = { type SettingsDataModelFieldAddressFormProps = {
disabled?: boolean; disabled?: boolean;
defaultCountry?: string; defaultCountry?: string;
@ -41,7 +46,7 @@ export const SettingsDataModelFieldAddressForm = ({
}, },
...useCountries() ...useCountries()
.sort((a, b) => a.countryName.localeCompare(b.countryName)) .sort((a, b) => a.countryName.localeCompare(b.countryName))
.map<SelectOption<string>>(({ countryName, Flag }) => ({ .map<SelectOption>(({ countryName, Flag }) => ({
label: countryName, label: countryName,
value: countryName, value: countryName,
Icon: (props: IconComponentProps) => Icon: (props: IconComponentProps) =>

View File

@ -1,7 +1,7 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { IconCircleOff, useIcons } from 'twenty-ui'; import { IconCircleOff, SelectOption, useIcons } from 'twenty-ui';
import { ZodError, isDirty, z } from 'zod'; import { ZodError, isDirty, z } from 'zod';
import { LABEL_IDENTIFIER_FIELD_METADATA_TYPES } from '@/object-metadata/constants/LabelIdentifierFieldMetadataTypes'; import { LABEL_IDENTIFIER_FIELD_METADATA_TYPES } from '@/object-metadata/constants/LabelIdentifierFieldMetadataTypes';
@ -11,7 +11,7 @@ import { getActiveFieldMetadataItems } from '@/object-metadata/utils/getActiveFi
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema'; import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Select, SelectOption } from '@/ui/input/components/Select'; import { Select } from '@/ui/input/components/Select';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';

View File

@ -1,15 +1,15 @@
import { defaultSpreadsheetImportProps } from '@/spreadsheet-import/provider/components/SpreadsheetImport'; import { defaultSpreadsheetImportProps } from '@/spreadsheet-import/provider/components/SpreadsheetImport';
import { Columns } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { import {
Fields,
SpreadsheetImportDialogOptions, SpreadsheetImportDialogOptions,
SpreadsheetImportFields
} from '@/spreadsheet-import/types'; } from '@/spreadsheet-import/types';
import { sleep } from '~/utils/sleep'; import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { sleep } from '~/utils/sleep';
const fields = [ const fields = [
{ {
icon: null, Icon: null,
label: 'Name', label: 'Name',
key: 'name', key: 'name',
alternateMatches: ['first name', 'first'], alternateMatches: ['first name', 'first'],
@ -26,7 +26,7 @@ const fields = [
fieldMetadataType: FieldMetadataType.TEXT, fieldMetadataType: FieldMetadataType.TEXT,
}, },
{ {
icon: null, Icon: null,
label: 'Surname', label: 'Surname',
key: 'surname', key: 'surname',
alternateMatches: ['second name', 'last name', 'last'], alternateMatches: ['second name', 'last name', 'last'],
@ -44,7 +44,7 @@ const fields = [
description: 'Family / Last name', description: 'Family / Last name',
}, },
{ {
icon: null, Icon: null,
label: 'Age', label: 'Age',
key: 'age', key: 'age',
alternateMatches: ['years'], alternateMatches: ['years'],
@ -62,7 +62,7 @@ const fields = [
], ],
}, },
{ {
icon: null, Icon: null,
label: 'Team', label: 'Team',
key: 'team', key: 'team',
alternateMatches: ['department'], alternateMatches: ['department'],
@ -82,7 +82,7 @@ const fields = [
], ],
}, },
{ {
icon: null, Icon: null,
label: 'Is manager', label: 'Is manager',
key: 'is_manager', key: 'is_manager',
alternateMatches: ['manages'], alternateMatches: ['manages'],
@ -92,9 +92,9 @@ const fields = [
}, },
example: 'true', example: 'true',
}, },
] as Fields<string>; ] as SpreadsheetImportFields<string>;
export const importedColums: Columns<string> = [ export const importedColums: SpreadsheetColumns<string> = [
{ {
header: 'Name', header: 'Name',
index: 0, index: 0,

View File

@ -9,11 +9,10 @@ import {
} from '@floating-ui/react'; } from '@floating-ui/react';
import React, { useCallback, useRef, useState } from 'react'; import React, { useCallback, useRef, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { AppTooltip, MenuItem, MenuItemSelect } from 'twenty-ui'; import { AppTooltip, MenuItem, MenuItemSelect, SelectOption } from 'twenty-ui';
import { ReadonlyDeep } from 'type-fest'; import { ReadonlyDeep } from 'type-fest';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import { SelectOption } from '@/spreadsheet-import/types';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
@ -21,6 +20,7 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer'; import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { v4 as uuidV4 } from 'uuid';
import { useUpdateEffect } from '~/hooks/useUpdateEffect'; import { useUpdateEffect } from '~/hooks/useUpdateEffect';
const StyledFloatingDropdown = styled.div` const StyledFloatingDropdown = styled.div`
@ -116,7 +116,7 @@ export const MatchColumnSelect = ({
<> <>
<div ref={refs.setReference}> <div ref={refs.setReference}>
<MenuItem <MenuItem
LeftIcon={value?.icon} LeftIcon={value?.Icon}
onClick={handleDropdownItemClick} onClick={handleDropdownItemClick}
text={value?.label ?? placeholder ?? ''} text={value?.label ?? placeholder ?? ''}
accent={value?.label ? 'default' : 'placeholder'} accent={value?.label ? 'default' : 'placeholder'}
@ -138,31 +138,36 @@ export const MatchColumnSelect = ({
/> />
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight> <DropdownMenuItemsContainer hasMaxHeight>
{options?.map((option) => ( {options?.map((option) => {
<React.Fragment key={option.label}> const id = `${uuidV4()}-${option.value}`;
<MenuItemSelect return (
selected={value?.label === option.label} <React.Fragment key={id}>
onClick={() => handleChange(option)} <div id={id}>
disabled={ <MenuItemSelect
option.disabled && value?.value !== option.value selected={value?.label === option.label}
} onClick={() => handleChange(option)}
LeftIcon={option?.icon} disabled={
text={option.label} option.disabled && value?.value !== option.value
/> }
{option.disabled && LeftIcon={option?.Icon}
value?.value !== option.value && text={option.label}
createPortal( />
<AppTooltip </div>
key={option.value} {option.disabled &&
anchorSelect={`#${option.value}`} value?.value !== option.value &&
content={t`You are already importing this column.`} createPortal(
place="right" <AppTooltip
offset={-20} key={id}
/>, anchorSelect={`#${id}`}
document.body, content={t`You are already importing this column.`}
)} place="right"
</React.Fragment> offset={-20}
))} />,
document.body,
)}
</React.Fragment>
);
})}
{options?.length === 0 && ( {options?.length === 0 && (
<MenuItem key="No results" text={t`No results`} /> <MenuItem key="No results" text={t`No results`} />
)} )}

View File

@ -5,9 +5,9 @@ import { Heading } from '@/spreadsheet-import/components/Heading';
import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton'; import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { import {
Field,
ImportedRow, ImportedRow,
ImportedStructuredRow, ImportedStructuredRow,
SpreadsheetImportField,
} from '@/spreadsheet-import/types'; } from '@/spreadsheet-import/types';
import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields'; import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields';
import { getMatchedColumns } from '@/spreadsheet-import/utils/getMatchedColumns'; import { getMatchedColumns } from '@/spreadsheet-import/utils/getMatchedColumns';
@ -25,6 +25,9 @@ import { initialComputedColumnsSelector } from '@/spreadsheet-import/steps/compo
import { UnmatchColumn } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumn'; import { UnmatchColumn } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumn';
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';
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { Trans, useLingui } from '@lingui/react/macro'; import { Trans, useLingui } from '@lingui/react/macro';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
@ -68,68 +71,6 @@ export type MatchColumnsStepProps = {
onError: (message: string) => void; onError: (message: string) => void;
}; };
export enum ColumnType {
empty,
ignored,
matched,
matchedCheckbox,
matchedSelect,
matchedSelectOptions,
}
export type MatchedOptions<T> = {
entry: string;
value?: T;
};
type EmptyColumn = { type: ColumnType.empty; index: number; header: string };
type IgnoredColumn = {
type: ColumnType.ignored;
index: number;
header: string;
};
type MatchedColumn<T> = {
type: ColumnType.matched;
index: number;
header: string;
value: T;
};
type MatchedSwitchColumn<T> = {
type: ColumnType.matchedCheckbox;
index: number;
header: string;
value: T;
};
export type MatchedSelectColumn<T> = {
type: ColumnType.matchedSelect;
index: number;
header: string;
value: T;
matchedOptions: Partial<MatchedOptions<T>>[];
};
export type MatchedSelectOptionsColumn<T> = {
type: ColumnType.matchedSelectOptions;
index: number;
header: string;
value: T;
matchedOptions: MatchedOptions<T>[];
};
export type Column<T extends string> =
| EmptyColumn
| IgnoredColumn
| MatchedColumn<T>
| MatchedSwitchColumn<T>
| MatchedSelectColumn<T>
| MatchedSelectOptionsColumn<T>;
export type Columns<T extends string> = Column<T>[];
export const MatchColumnsStep = <T extends string>({ export const MatchColumnsStep = <T extends string>({
data, data,
headerValues, headerValues,
@ -179,7 +120,7 @@ export const MatchColumnsStep = <T extends string>({
const onChange = useCallback( const onChange = useCallback(
(value: T, columnIndex: number) => { (value: T, columnIndex: number) => {
if (value === 'do-not-import') { if (value === 'do-not-import') {
if (columns[columnIndex].type === ColumnType.ignored) { if (columns[columnIndex].type === SpreadsheetColumnType.ignored) {
onRevertIgnore(columnIndex); onRevertIgnore(columnIndex);
} else { } else {
onIgnore(columnIndex); onIgnore(columnIndex);
@ -187,12 +128,12 @@ export const MatchColumnsStep = <T extends string>({
} else { } else {
const field = fields.find( const field = fields.find(
(field) => field.key === value, (field) => field.key === value,
) as unknown as Field<T>; ) 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<Column<string>>((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) {
@ -223,7 +164,7 @@ export const MatchColumnsStep = <T extends string>({
async ( async (
values: ImportedStructuredRow<string>[], values: ImportedStructuredRow<string>[],
rawData: ImportedRow[], rawData: ImportedRow[],
columns: Columns<string>, columns: SpreadsheetColumns<string>,
) => { ) => {
try { try {
const data = await matchColumnsStepHook(values, rawData, columns); const data = await matchColumnsStepHook(values, rawData, columns);
@ -322,7 +263,7 @@ export const MatchColumnsStep = <T extends string>({
useEffect(() => { useEffect(() => {
const isInitialColumnsState = columns.every( const isInitialColumnsState = columns.every(
(column) => column.type === ColumnType.empty, (column) => column.type === SpreadsheetColumnType.empty,
); );
if (autoMapHeaders && isInitialColumnsState) { if (autoMapHeaders && isInitialColumnsState) {
setColumns(getMatchedColumns(columns, fields, data, autoMapDistance)); setColumns(getMatchedColumns(columns, fields, data, autoMapDistance));

View File

@ -1,8 +1,7 @@
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import React from 'react'; import React from 'react';
import { Columns } from '../MatchColumnsStep';
const StyledGridContainer = styled.div` const StyledGridContainer = styled.div`
align-items: center; align-items: center;
display: flex; display: flex;
@ -93,17 +92,17 @@ const StyledGridHeader = styled.div<PositionProps>`
`; `;
type ColumnGridProps<T extends string> = { type ColumnGridProps<T extends string> = {
columns: Columns<T>; columns: SpreadsheetColumns<T>;
renderUserColumn: ( renderUserColumn: (
columns: Columns<T>, columns: SpreadsheetColumns<T>,
columnIndex: number, columnIndex: number,
) => React.ReactNode; ) => React.ReactNode;
renderTemplateColumn: ( renderTemplateColumn: (
columns: Columns<T>, columns: SpreadsheetColumns<T>,
columnIndex: number, columnIndex: number,
) => React.ReactNode; ) => React.ReactNode;
renderUnmatchedColumn: ( renderUnmatchedColumn: (
columns: Columns<T>, columns: SpreadsheetColumns<T>,
columnIndex: number, columnIndex: number,
) => React.ReactNode; ) => React.ReactNode;
}; };

View File

@ -2,20 +2,19 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { SelectOption } from '@/spreadsheet-import/types';
import { getFieldOptions } from '@/spreadsheet-import/utils/getFieldOptions'; import { getFieldOptions } from '@/spreadsheet-import/utils/getFieldOptions';
import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope'; import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope';
import {
SpreadsheetMatchedSelectColumn,
SpreadsheetMatchedSelectOptionsColumn,
} from '@/spreadsheet-import/types/SpreadsheetColumn';
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
import { SelectInput } from '@/ui/input/components/SelectInput'; import { SelectInput } from '@/ui/input/components/SelectInput';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { IconChevronDown, Tag, TagColor } from 'twenty-ui'; import { IconChevronDown, SelectOption, Tag, TagColor } from 'twenty-ui';
import {
MatchedOptions,
MatchedSelectColumn,
MatchedSelectOptionsColumn,
} from '../MatchColumnsStep';
const StyledContainer = styled.div` const StyledContainer = styled.div`
align-items: center; align-items: center;
@ -58,11 +57,15 @@ const StyledIconChevronDown = styled(IconChevronDown)`
`; `;
interface SubMatchingSelectProps<T> { interface SubMatchingSelectProps<T> {
option: MatchedOptions<T> | Partial<MatchedOptions<T>>; option: SpreadsheetMatchedOptions<T> | Partial<SpreadsheetMatchedOptions<T>>;
column: MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T>; column:
| SpreadsheetMatchedSelectColumn<T>
| SpreadsheetMatchedSelectOptionsColumn<T>;
onSubChange: (val: T, index: number, option: string) => void; onSubChange: (val: T, index: number, option: string) => void;
placeholder: string; placeholder: string;
selectedOption?: MatchedOptions<T> | Partial<MatchedOptions<T>>; selectedOption?:
| SpreadsheetMatchedOptions<T>
| Partial<SpreadsheetMatchedOptions<T>>;
} }
export const SubMatchingSelect = <T extends string>({ export const SubMatchingSelect = <T extends string>({

View File

@ -3,9 +3,10 @@ import { IconForbid } from 'twenty-ui';
import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect'; import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { Columns, ColumnType } from '../MatchColumnsStep';
const StyledContainer = styled.div` const StyledContainer = styled.div`
display: flex; display: flex;
@ -15,7 +16,7 @@ const StyledContainer = styled.div`
`; `;
type TemplateColumnProps<T extends string> = { type TemplateColumnProps<T extends string> = {
columns: Columns<string>; columns: SpreadsheetColumns<string>;
columnIndex: number; columnIndex: number;
onChange: (val: T, index: number) => void; onChange: (val: T, index: number) => void;
}; };
@ -27,13 +28,13 @@ export const TemplateColumn = <T extends string>({
}: TemplateColumnProps<T>) => { }: TemplateColumnProps<T>) => {
const { fields } = useSpreadsheetImportInternal<T>(); const { fields } = useSpreadsheetImportInternal<T>();
const column = columns[columnIndex]; const column = columns[columnIndex];
const isIgnored = column.type === ColumnType.ignored; const isIgnored = column.type === SpreadsheetColumnType.ignored;
const { t } = useLingui(); const { t } = useLingui();
const fieldOptions = fields const fieldOptions = fields
.filter((field) => field.fieldMetadataType !== FieldMetadataType.RICH_TEXT) .filter((field) => field.fieldMetadataType !== FieldMetadataType.RICH_TEXT)
.map(({ icon, label, key }) => { .map(({ Icon, label, key }) => {
const isSelected = const isSelected =
columns.findIndex((column) => { columns.findIndex((column) => {
if ('value' in column) { if ('value' in column) {
@ -43,7 +44,7 @@ export const TemplateColumn = <T extends string>({
}) !== -1; }) !== -1;
return { return {
icon: icon, Icon: Icon,
value: key, value: key,
label: label, label: label,
disabled: isSelected, disabled: isSelected,
@ -52,7 +53,7 @@ export const TemplateColumn = <T extends string>({
const selectOptions = [ const selectOptions = [
{ {
icon: IconForbid, Icon: IconForbid,
value: 'do-not-import', value: 'do-not-import',
label: t`Do not import`, label: t`Do not import`,
}, },

View File

@ -1,8 +1,9 @@
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { SubMatchingSelect } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelect'; import { SubMatchingSelect } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelect';
import { UnmatchColumnBanner } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumnBanner'; import { UnmatchColumnBanner } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumnBanner';
import { Column } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
import { Fields } from '@/spreadsheet-import/types'; import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { useState } from 'react'; import { useState } from 'react';
@ -10,8 +11,8 @@ import { isDefined } from 'twenty-shared/utils';
import { AnimatedExpandableContainer } from 'twenty-ui'; import { AnimatedExpandableContainer } from 'twenty-ui';
const getExpandableContainerTitle = <T extends string>( const getExpandableContainerTitle = <T extends string>(
fields: Fields<T>, fields: SpreadsheetImportFields<T>,
column: Column<T>, 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,
@ -24,7 +25,7 @@ const getExpandableContainerTitle = <T extends string>(
}; };
type UnmatchColumnProps<T extends string> = { type UnmatchColumnProps<T extends string> = {
columns: Column<T>[]; columns: SpreadsheetColumns<T>;
columnIndex: number; columnIndex: number;
onSubChange: (val: T, index: number, option: string) => void; onSubChange: (val: T, index: number, option: string) => void;
}; };

View File

@ -2,7 +2,7 @@ import styled from '@emotion/styled';
import { ImportedRow } from '@/spreadsheet-import/types'; import { ImportedRow } from '@/spreadsheet-import/types';
import { Column } from '../MatchColumnsStep'; import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -30,7 +30,7 @@ const StyledExample = styled.span`
`; `;
type UserTableColumnProps<T extends string> = { type UserTableColumnProps<T extends string> = {
column: Column<T>; column: SpreadsheetColumn<T>;
importedRow: ImportedRow; importedRow: ImportedRow;
}; };

View File

@ -1,34 +1,32 @@
import {
Columns,
ColumnType,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { ImportedRow } from '@/spreadsheet-import/types'; import { ImportedRow } from '@/spreadsheet-import/types';
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { atom, selectorFamily } from 'recoil'; import { atom, selectorFamily } from 'recoil';
export const matchColumnsState = atom({ export const matchColumnsState = atom({
key: 'MatchColumnsState', key: 'MatchColumnsState',
default: [] as Columns<string>, default: [] as SpreadsheetColumns<string>,
}); });
export const initialComputedColumnsSelector = selectorFamily< export const initialComputedColumnsSelector = selectorFamily<
Columns<string>, SpreadsheetColumns<string>,
ImportedRow ImportedRow
>({ >({
key: 'initialComputedColumnsSelector', key: 'initialComputedColumnsSelector',
get: get:
(headerValues: ImportedRow) => (headerValues: ImportedRow) =>
({ get }) => { ({ get }) => {
const currentState = get(matchColumnsState) as Columns<string>; 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(
(value, index) => ({ (value, index) => ({
type: ColumnType.empty, type: SpreadsheetColumnType.empty,
index, index,
header: value ?? '', header: value ?? '',
}), }),
); );
return initialState as Columns<string>; return initialState as SpreadsheetColumns<string>;
} else { } else {
return currentState; return currentState;
} }
@ -36,6 +34,6 @@ export const initialComputedColumnsSelector = selectorFamily<
set: set:
() => () =>
({ set }, newValue) => { ({ set }, newValue) => {
set(matchColumnsState, newValue as Columns<string>); set(matchColumnsState, newValue as SpreadsheetColumns<string>);
}, },
}); });

View File

@ -1,13 +1,13 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { SpreadsheetImportTable } from '@/spreadsheet-import/components/SpreadsheetImportTable'; import { SpreadsheetImportTable } from '@/spreadsheet-import/components/SpreadsheetImportTable';
import { Fields } from '@/spreadsheet-import/types'; import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
import { generateExampleRow } from '@/spreadsheet-import/utils/generateExampleRow'; import { generateExampleRow } from '@/spreadsheet-import/utils/generateExampleRow';
import { generateColumns } from './columns'; import { generateColumns } from './columns';
interface ExampleTableProps<T extends string> { interface ExampleTableProps<T extends string> {
fields: Fields<T>; fields: SpreadsheetImportFields<T>;
} }
export const ExampleTable = <T extends string>({ export const ExampleTable = <T extends string>({

View File

@ -1,10 +1,10 @@
import styled from '@emotion/styled';
// @ts-expect-error // Todo: remove usage of react-data-grid // @ts-expect-error // Todo: remove usage of react-data-grid
import { Column } from 'react-data-grid'; import { Column } from 'react-data-grid';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import styled from '@emotion/styled';
import { AppTooltip } from 'twenty-ui'; import { AppTooltip } from 'twenty-ui';
import { Fields } from '@/spreadsheet-import/types'; import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
const StyledHeaderContainer = styled.div` const StyledHeaderContainer = styled.div`
align-items: center; align-items: center;
@ -27,7 +27,9 @@ const StyledDefaultContainer = styled.div`
text-overflow: ellipsis; text-overflow: ellipsis;
`; `;
export const generateColumns = <T extends string>(fields: Fields<T>) => export const generateColumns = <T extends string>(
fields: SpreadsheetImportFields<T>,
) =>
fields.map( fields.map(
(column): Column<any> => ({ (column): Column<any> => ({
key: column.key, key: column.key,

View File

@ -2,16 +2,14 @@ import { Heading } from '@/spreadsheet-import/components/Heading';
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 { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import {
ColumnType,
Columns,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
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';
import { import {
ImportValidationResult,
ImportedStructuredRow, ImportedStructuredRow,
SpreadsheetImportImportValidationResult,
} from '@/spreadsheet-import/types'; } from '@/spreadsheet-import/types';
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations'; import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations';
import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager'; import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager';
import { Modal } from '@/ui/layout/modal/components/Modal'; import { Modal } from '@/ui/layout/modal/components/Modal';
@ -74,7 +72,7 @@ const StyledNoRowsContainer = styled.div`
type ValidationStepProps<T extends string> = { type ValidationStepProps<T extends string> = {
initialData: ImportedStructuredRow<T>[]; initialData: ImportedStructuredRow<T>[];
importedColumns: Columns<string>; importedColumns: SpreadsheetColumns<string>;
file: File; file: File;
onBack: () => void; onBack: () => void;
setCurrentStepState: Dispatch<SetStateAction<SpreadsheetImportStep>>; setCurrentStepState: Dispatch<SetStateAction<SpreadsheetImportStep>>;
@ -153,13 +151,14 @@ export const ValidationStep = <T extends string>({
const hasBeenImported = const hasBeenImported =
importedColumns.filter( importedColumns.filter(
(importColumn) => (importColumn) =>
(importColumn.type === ColumnType.matched && (importColumn.type === SpreadsheetColumnType.matched &&
importColumn.value === column.key) || importColumn.value === column.key) ||
(importColumn.type === ColumnType.matchedSelect && (importColumn.type === SpreadsheetColumnType.matchedSelect &&
importColumn.value === column.key) || importColumn.value === column.key) ||
(importColumn.type === ColumnType.matchedSelectOptions && (importColumn.type ===
SpreadsheetColumnType.matchedSelectOptions &&
importColumn.value === column.key) || importColumn.value === column.key) ||
(importColumn.type === ColumnType.matchedCheckbox && (importColumn.type === SpreadsheetColumnType.matchedCheckbox &&
importColumn.value === column.key) || importColumn.value === column.key) ||
column.key === 'select-row', column.key === 'select-row',
).length > 0; ).length > 0;
@ -214,7 +213,7 @@ export const ValidationStep = <T extends string>({
validStructuredRows: [] as ImportedStructuredRow<T>[], validStructuredRows: [] as ImportedStructuredRow<T>[],
invalidStructuredRows: [] as ImportedStructuredRow<T>[], invalidStructuredRows: [] as ImportedStructuredRow<T>[],
allStructuredRows: data, allStructuredRows: data,
} satisfies ImportValidationResult<T>, } satisfies SpreadsheetImportImportValidationResult<T>,
); );
setCurrentStepState({ setCurrentStepState({

View File

@ -5,11 +5,14 @@ import { createPortal } from 'react-dom';
import { AppTooltip, Checkbox, CheckboxVariant, Toggle } from 'twenty-ui'; import { AppTooltip, Checkbox, CheckboxVariant, Toggle } from 'twenty-ui';
import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect'; import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
import { Fields, ImportedStructuredRow } from '@/spreadsheet-import/types'; import {
ImportedStructuredRow,
SpreadsheetImportFields,
} from '@/spreadsheet-import/types';
import { TextInput } from '@/ui/input/components/TextInput'; import { TextInput } from '@/ui/input/components/TextInput';
import { ImportedStructuredRowMetadata } from '../types';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { ImportedStructuredRowMetadata } from '../types';
const StyledHeaderContainer = styled.div` const StyledHeaderContainer = styled.div`
align-items: center; align-items: center;
@ -60,7 +63,7 @@ const StyledDefaultContainer = styled.div`
const SELECT_COLUMN_KEY = 'select-row'; const SELECT_COLUMN_KEY = 'select-row';
export const generateColumns = <T extends string>( export const generateColumns = <T extends string>(
fields: Fields<T>, fields: SpreadsheetImportFields<T>,
): Column<ImportedStructuredRow<T> & ImportedStructuredRowMetadata>[] => [ ): Column<ImportedStructuredRow<T> & ImportedStructuredRowMetadata>[] => [
{ {
key: SELECT_COLUMN_KEY, key: SELECT_COLUMN_KEY,
@ -135,7 +138,7 @@ export const generateColumns = <T extends string>(
value={ value={
value value
? ({ ? ({
icon: undefined, Icon: undefined,
...value, ...value,
} as const) } as const)
: value : value

View File

@ -1,8 +1,8 @@
import { Info } from '@/spreadsheet-import/types'; import { SpreadsheetImportInfo } from '@/spreadsheet-import/types';
export type ImportedStructuredRowMetadata = { export type ImportedStructuredRowMetadata = {
__index: string; __index: string;
__errors?: Error | null; __errors?: Error | null;
}; };
export type Error = { [key: string]: Info }; export type Error = { [key: string]: SpreadsheetImportInfo };
export type Errors = { [id: string]: Error }; export type Errors = { [id: string]: Error };

View File

@ -1,6 +1,6 @@
import { Columns } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType'; import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType';
import { ImportedRow } from '@/spreadsheet-import/types'; import { ImportedRow } from '@/spreadsheet-import/types';
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
import { WorkBook } from 'xlsx-ugnis'; import { WorkBook } from 'xlsx-ugnis';
export type SpreadsheetImportStep = export type SpreadsheetImportStep =
@ -23,7 +23,7 @@ export type SpreadsheetImportStep =
| { | {
type: SpreadsheetImportStepType.validateData; type: SpreadsheetImportStepType.validateData;
data: any[]; data: any[];
importedColumns: Columns<string>; importedColumns: SpreadsheetColumns<string>;
} }
| { | {
type: SpreadsheetImportStepType.loading; type: SpreadsheetImportStepType.loading;

View File

@ -0,0 +1,52 @@
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
type SpreadsheetEmptyColumn = {
type: SpreadsheetColumnType.empty;
index: number;
header: string;
};
type SpreadsheetIgnoredColumn = {
type: SpreadsheetColumnType.ignored;
index: number;
header: string;
};
type SpreadsheetMatchedColumn<T> = {
type: SpreadsheetColumnType.matched;
index: number;
header: string;
value: T;
};
type SpreadsheetMatchedSwitchColumn<T> = {
type: SpreadsheetColumnType.matchedCheckbox;
index: number;
header: string;
value: T;
};
export type SpreadsheetMatchedSelectColumn<T> = {
type: SpreadsheetColumnType.matchedSelect;
index: number;
header: string;
value: T;
matchedOptions: Partial<SpreadsheetMatchedOptions<T>>[];
};
export type SpreadsheetMatchedSelectOptionsColumn<T> = {
type: SpreadsheetColumnType.matchedSelectOptions;
index: number;
header: string;
value: T;
matchedOptions: SpreadsheetMatchedOptions<T>[];
};
export type SpreadsheetColumn<T extends string> =
| SpreadsheetEmptyColumn
| SpreadsheetIgnoredColumn
| SpreadsheetMatchedColumn<T>
| SpreadsheetMatchedSwitchColumn<T>
| SpreadsheetMatchedSelectColumn<T>
| SpreadsheetMatchedSelectOptionsColumn<T>;

View File

@ -0,0 +1,8 @@
export enum SpreadsheetColumnType {
empty,
ignored,
matched,
matchedCheckbox,
matchedSelect,
matchedSelectOptions,
}

View File

@ -0,0 +1,3 @@
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
export type SpreadsheetColumns<T extends string> = SpreadsheetColumn<T>[];

View File

@ -0,0 +1,61 @@
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
import { SpreadsheetImportFields } from '@/spreadsheet-import/types/SpreadsheetImportFields';
import { ImportedRow } from '@/spreadsheet-import/types/SpreadsheetImportImportedRow';
import { ImportedStructuredRow } from '@/spreadsheet-import/types/SpreadsheetImportImportedStructuredRow';
import { SpreadsheetImportImportValidationResult } from '@/spreadsheet-import/types/SpreadsheetImportImportValidationResult';
import { SpreadsheetImportRowHook } from '@/spreadsheet-import/types/SpreadsheetImportRowHook';
import { SpreadsheetImportTableHook } from '@/spreadsheet-import/types/SpreadsheetImportTableHook';
import { SpreadsheetImportStep } from '../steps/types/SpreadsheetImportStep';
export type SpreadsheetImportDialogOptions<FieldNames extends string> = {
// Is modal visible.
isOpen: boolean;
// callback when RSI is closed before final submit
onClose: () => void;
// Field description for requested data
fields: SpreadsheetImportFields<FieldNames>;
// Runs after file upload step, receives and returns raw sheet data
uploadStepHook?: (importedRows: ImportedRow[]) => Promise<ImportedRow[]>;
// Runs after header selection step, receives and returns raw sheet data
selectHeaderStepHook?: (
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
matchColumnsStepHook?: (
importedStructuredRows: ImportedStructuredRow<FieldNames>[],
importedRows: ImportedRow[],
columns: SpreadsheetColumns<FieldNames>,
) => Promise<ImportedStructuredRow<FieldNames>[]>;
// Runs after column matching and on entry change
rowHook?: SpreadsheetImportRowHook<FieldNames>;
// Runs after column matching and on entry change
tableHook?: SpreadsheetImportTableHook<FieldNames>;
// Function called after user finishes the flow
onSubmit: (
validationResult: SpreadsheetImportImportValidationResult<FieldNames>,
file: File,
) => Promise<void>;
// Allows submitting with errors. Default: true
allowInvalidSubmit?: boolean;
// Theme configuration passed to underlying Chakra-UI
customTheme?: object;
// Specifies maximum number of rows for a single import
maxRecords?: number;
// Maximum upload filesize (in bytes)
maxFileSize?: number;
// Automatically map imported headers to specified fields if possible. Default: true
autoMapHeaders?: boolean;
// Headers matching accuracy: 1 for strict and up for more flexible matching
autoMapDistance?: number;
// Initial Step state to be rendered on load
initialStepState?: SpreadsheetImportStep;
// Sets SheetJS dateNF option. If date parsing is applied, date will be formatted e.g. "yyyy-mm-dd hh:mm:ss", "m/d/yy h:mm", 'mmm-yy', etc.
dateFormat?: string;
// Sets SheetJS "raw" option. If true, parsing will only be applied to xlsx date fields.
parseRaw?: boolean;
// Use for right-to-left (RTL) support
rtl?: boolean;
// Allow header selection
selectHeader?: boolean;
};

View File

@ -0,0 +1 @@
export type SpreadsheetImportErrorLevel = 'info' | 'warning' | 'error';

View File

@ -0,0 +1,25 @@
import { SpreadsheetImportFieldType } from '@/spreadsheet-import/types/SpreadsheetImportFieldType';
import { SpreadsheetImportFieldValidationDefinition } from '@/spreadsheet-import/types/SpreadsheetImportFieldValidationDefinition';
import { FieldMetadataType } from 'twenty-shared/types';
import { IconComponent } from 'twenty-ui';
export type SpreadsheetImportField<T extends string> = {
// Icon
Icon: IconComponent | null | undefined;
// UI-facing field label
label: string;
// Field's unique identifier
key: T;
// UI-facing additional information displayed via tooltip and ? icon
description?: string;
// Alternate labels used for fields' auto-matching, e.g. "fname" -> "firstName"
alternateMatches?: string[];
// Validations used for field entries
fieldValidationDefinitions?: SpreadsheetImportFieldValidationDefinition[];
// Field entry component, default: Input
fieldType: SpreadsheetImportFieldType;
// Field metadata type
fieldMetadataType: FieldMetadataType;
// UI-facing values shown to user as field examples pre-upload phase
example?: string;
};

View File

@ -0,0 +1,28 @@
import { SelectOption } from 'twenty-ui';
export type SpreadsheetImportCheckbox = {
type: 'checkbox';
// Alternate values to be treated as booleans, e.g. {yes: true, no: false}
booleanMatches?: { [key: string]: boolean };
};
export type SpreadsheetImportSelect = {
type: 'select';
// Options displayed in Select component
options: SelectOption[];
};
export type SpreadsheetImportMultiSelect = {
type: 'multiSelect';
options: SelectOption[];
};
export type SpreadsheetImportInput = {
type: 'input';
};
export type SpreadsheetImportFieldType =
| SpreadsheetImportCheckbox
| SpreadsheetImportSelect
| SpreadsheetImportMultiSelect
| SpreadsheetImportInput;

View File

@ -0,0 +1,43 @@
import { SpreadsheetImportErrorLevel } from '@/spreadsheet-import/types/SpreadsheetImportErrorLevel';
export type SpreadsheetImportRequiredValidation = {
rule: 'required';
errorMessage?: string;
level?: SpreadsheetImportErrorLevel;
};
export type SpreadsheetImportUniqueValidation = {
rule: 'unique';
allowEmpty?: boolean;
errorMessage?: string;
level?: SpreadsheetImportErrorLevel;
};
export type SpreadsheetImportRegexValidation = {
rule: 'regex';
value: string;
flags?: string;
errorMessage: string;
level?: SpreadsheetImportErrorLevel;
};
export type SpreadsheetImportFunctionValidation = {
rule: 'function';
isValid: (value: string) => boolean;
errorMessage: string;
level?: SpreadsheetImportErrorLevel;
};
export type SpreadsheetImportObjectValidation = {
rule: 'object';
isValid: (objectValue: any) => boolean;
errorMessage: string;
level?: SpreadsheetImportErrorLevel;
};
export type SpreadsheetImportFieldValidationDefinition =
| SpreadsheetImportRequiredValidation
| SpreadsheetImportUniqueValidation
| SpreadsheetImportRegexValidation
| SpreadsheetImportFunctionValidation
| SpreadsheetImportObjectValidation;

View File

@ -0,0 +1,6 @@
import { SpreadsheetImportField } from '@/spreadsheet-import/types/SpreadsheetImportField';
import { ReadonlyDeep } from 'type-fest';
export type SpreadsheetImportFields<T extends string> = ReadonlyDeep<
SpreadsheetImportField<T>[]
>;

View File

@ -0,0 +1,9 @@
import { ImportedStructuredRowMetadata } from '@/spreadsheet-import/steps/components/ValidationStep/types';
import { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow';
export type SpreadsheetImportImportValidationResult<T extends string> = {
validStructuredRows: ImportedStructuredRow<T>[];
invalidStructuredRows: ImportedStructuredRow<T>[];
allStructuredRows: (ImportedStructuredRow<T> &
ImportedStructuredRowMetadata)[];
};

View File

@ -0,0 +1 @@
export type ImportedRow = Array<string | undefined>;

View File

@ -0,0 +1,3 @@
export type ImportedStructuredRow<T extends string> = {
[key in T]: string | boolean | undefined;
};

View File

@ -0,0 +1,6 @@
import { SpreadsheetImportErrorLevel } from './SpreadsheetImportErrorLevel';
export type SpreadsheetImportInfo = {
message: string;
level: SpreadsheetImportErrorLevel;
};

View File

@ -0,0 +1,8 @@
import { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow';
import { SpreadsheetImportInfo } from './SpreadsheetImportInfo';
export type SpreadsheetImportRowHook<T extends string> = (
row: ImportedStructuredRow<T>,
addError: (fieldKey: T, error: SpreadsheetImportInfo) => void,
table: ImportedStructuredRow<T>[],
) => ImportedStructuredRow<T>;

View File

@ -0,0 +1,11 @@
import { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow';
import { SpreadsheetImportInfo } from './SpreadsheetImportInfo';
export type SpreadsheetImportTableHook<T extends string> = (
table: ImportedStructuredRow<T>[],
addError: (
rowIndex: number,
fieldKey: T,
error: SpreadsheetImportInfo,
) => void,
) => ImportedStructuredRow<T>[];

View File

@ -0,0 +1,4 @@
export type SpreadsheetMatchedOptions<T> = {
entry: string;
value?: T;
};

View File

@ -1,197 +1,27 @@
import { IconComponent, ThemeColor } from 'twenty-ui'; // Import all types we need for re-export or alias
import { ReadonlyDeep } from 'type-fest';
import { Columns } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; export type { SpreadsheetImportDialogOptions } from './SpreadsheetImportDialogOptions';
import { ImportedStructuredRowMetadata } from '@/spreadsheet-import/steps/components/ValidationStep/types'; export type { SpreadsheetImportErrorLevel } from './SpreadsheetImportErrorLevel';
import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep'; export type { SpreadsheetImportField } from './SpreadsheetImportField';
import { FieldMetadataType } from 'twenty-shared/types'; export type { SpreadsheetImportFields } from './SpreadsheetImportFields';
export type {
export type SpreadsheetImportDialogOptions<FieldNames extends string> = { SpreadsheetImportCheckbox,
// Is modal visible. SpreadsheetImportFieldType,
isOpen: boolean; SpreadsheetImportInput,
// callback when RSI is closed before final submit SpreadsheetImportMultiSelect,
onClose: () => void; SpreadsheetImportSelect,
// Field description for requested data } from './SpreadsheetImportFieldType';
fields: Fields<FieldNames>; export type {
// Runs after file upload step, receives and returns raw sheet data SpreadsheetImportFieldValidationDefinition,
uploadStepHook?: (importedRows: ImportedRow[]) => Promise<ImportedRow[]>; SpreadsheetImportFunctionValidation,
// Runs after header selection step, receives and returns raw sheet data SpreadsheetImportObjectValidation,
selectHeaderStepHook?: ( SpreadsheetImportRegexValidation,
headerRow: ImportedRow, SpreadsheetImportRequiredValidation,
importedRows: ImportedRow[], SpreadsheetImportUniqueValidation,
) => Promise<{ headerRow: ImportedRow; importedRows: ImportedRow[] }>; } from './SpreadsheetImportFieldValidationDefinition';
// Runs once before validation step, used for data mutations and if you want to change how columns were matched export type { ImportedRow } from './SpreadsheetImportImportedRow';
matchColumnsStepHook?: ( export type { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow';
importedStructuredRows: ImportedStructuredRow<FieldNames>[], export type { SpreadsheetImportImportValidationResult } from './SpreadsheetImportImportValidationResult';
importedRows: ImportedRow[], export type { SpreadsheetImportInfo } from './SpreadsheetImportInfo';
columns: Columns<FieldNames>, export type { SpreadsheetImportRowHook } from './SpreadsheetImportRowHook';
) => Promise<ImportedStructuredRow<FieldNames>[]>; export type { SpreadsheetImportTableHook } from './SpreadsheetImportTableHook';
// Runs after column matching and on entry change
rowHook?: RowHook<FieldNames>;
// Runs after column matching and on entry change
tableHook?: TableHook<FieldNames>;
// Function called after user finishes the flow
onSubmit: (
validationResult: ImportValidationResult<FieldNames>,
file: File,
) => Promise<void>;
// Allows submitting with errors. Default: true
allowInvalidSubmit?: boolean;
// Theme configuration passed to underlying Chakra-UI
customTheme?: object;
// Specifies maximum number of rows for a single import
maxRecords?: number;
// Maximum upload filesize (in bytes)
maxFileSize?: number;
// Automatically map imported headers to specified fields if possible. Default: true
autoMapHeaders?: boolean;
// Headers matching accuracy: 1 for strict and up for more flexible matching
autoMapDistance?: number;
// Initial Step state to be rendered on load
initialStepState?: SpreadsheetImportStep;
// Sets SheetJS dateNF option. If date parsing is applied, date will be formatted e.g. "yyyy-mm-dd hh:mm:ss", "m/d/yy h:mm", 'mmm-yy', etc.
dateFormat?: string;
// Sets SheetJS "raw" option. If true, parsing will only be applied to xlsx date fields.
parseRaw?: boolean;
// Use for right-to-left (RTL) support
rtl?: boolean;
// Allow header selection
selectHeader?: boolean;
};
export type ImportedRow = Array<string | undefined>;
export type ImportedStructuredRow<T extends string> = {
[key in T]: string | boolean | undefined;
};
// Data model RSI uses for spreadsheet imports
export type Fields<T extends string> = ReadonlyDeep<Field<T>[]>;
export type Checkbox = {
type: 'checkbox';
// Alternate values to be treated as booleans, e.g. {yes: true, no: false}
booleanMatches?: { [key: string]: boolean };
};
export type Select = {
type: 'select';
// Options displayed in Select component
options: SelectOption[];
};
export type MultiSelect = {
type: 'multiSelect';
options: SelectOption[];
};
export type SelectOption = {
// Icon
icon?: IconComponent | null;
// UI-facing option label
label: string;
// Field entry matching criteria as well as select output
value: string;
// Disabled option when already select
disabled?: boolean;
// Option color
color?: ThemeColor | 'transparent';
};
export type Input = {
type: 'input';
};
export type SpreadsheetImportFieldType =
| Checkbox
| Select
| MultiSelect
| Input;
export type Field<T extends string> = {
// Icon
icon: IconComponent | null | undefined;
// UI-facing field label
label: string;
// Field's unique identifier
key: T;
// UI-facing additional information displayed via tooltip and ? icon
description?: string;
// Alternate labels used for fields' auto-matching, e.g. "fname" -> "firstName"
alternateMatches?: string[];
// Validations used for field entries
fieldValidationDefinitions?: FieldValidationDefinition[];
// Field entry component, default: Input
fieldType: SpreadsheetImportFieldType;
// Field metadata type
fieldMetadataType: FieldMetadataType;
// UI-facing values shown to user as field examples pre-upload phase
example?: string;
};
export type FieldValidationDefinition =
| RequiredValidation
| UniqueValidation
| RegexValidation
| FunctionValidation
| ObjectValidation;
export type ObjectValidation = {
rule: 'object';
isValid: (objectValue: any) => boolean;
errorMessage: string;
level?: ErrorLevel;
};
export type RequiredValidation = {
rule: 'required';
errorMessage?: string;
level?: ErrorLevel;
};
export type UniqueValidation = {
rule: 'unique';
allowEmpty?: boolean;
errorMessage?: string;
level?: ErrorLevel;
};
export type RegexValidation = {
rule: 'regex';
value: string;
flags?: string;
errorMessage: string;
level?: ErrorLevel;
};
export type FunctionValidation = {
rule: 'function';
isValid: (value: string) => boolean;
errorMessage: string;
level?: ErrorLevel;
};
export type RowHook<T extends string> = (
row: ImportedStructuredRow<T>,
addError: (fieldKey: T, error: Info) => void,
table: ImportedStructuredRow<T>[],
) => ImportedStructuredRow<T>;
export type TableHook<T extends string> = (
table: ImportedStructuredRow<T>[],
addError: (rowIndex: number, fieldKey: T, error: Info) => void,
) => ImportedStructuredRow<T>[];
export type ErrorLevel = 'info' | 'warning' | 'error';
export type Info = {
message: string;
level: ErrorLevel;
};
export type ImportValidationResult<T extends string> = {
validStructuredRows: ImportedStructuredRow<T>[];
invalidStructuredRows: ImportedStructuredRow<T>[];
allStructuredRows: (ImportedStructuredRow<T> &
ImportedStructuredRowMetadata)[];
};

View File

@ -1,45 +1,45 @@
import { import {
Field,
ImportedStructuredRow, ImportedStructuredRow,
Info, SpreadsheetImportField,
RowHook, SpreadsheetImportInfo,
TableHook, SpreadsheetImportRowHook,
SpreadsheetImportTableHook,
} from '@/spreadsheet-import/types'; } from '@/spreadsheet-import/types';
import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations'; import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
describe('addErrorsAndRunHooks', () => { describe('addErrorsAndRunHooks', () => {
type FullData = ImportedStructuredRow<'name' | 'age' | 'country'>; type FullData = ImportedStructuredRow<'name' | 'age' | 'country'>;
const requiredField: Field<'name'> = { 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,
}; };
const regexField: Field<'age'> = { const regexField: SpreadsheetImportField<'age'> = {
key: 'age', key: 'age',
label: 'Age', label: 'Age',
fieldValidationDefinitions: [ fieldValidationDefinitions: [
{ rule: 'regex', value: '\\d+', errorMessage: 'Regex error' }, { rule: 'regex', value: '\\d+', errorMessage: 'Regex error' },
], ],
icon: null, Icon: null,
fieldType: { type: 'input' }, fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.NUMBER, fieldMetadataType: FieldMetadataType.NUMBER,
}; };
const uniqueField: Field<'country'> = { 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,
}; };
const functionValidationFieldTrue: Field<'email'> = { const functionValidationFieldTrue: SpreadsheetImportField<'email'> = {
key: 'email', key: 'email',
label: 'Email', label: 'Email',
fieldValidationDefinitions: [ fieldValidationDefinitions: [
@ -49,12 +49,12 @@ describe('addErrorsAndRunHooks', () => {
errorMessage: 'Field is invalid', errorMessage: 'Field is invalid',
}, },
], ],
icon: null, Icon: null,
fieldType: { type: 'input' }, fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.EMAILS, fieldMetadataType: FieldMetadataType.EMAILS,
}; };
const functionValidationFieldFalse: Field<'email'> = { const functionValidationFieldFalse: SpreadsheetImportField<'email'> = {
key: 'email', key: 'email',
label: 'Email', label: 'Email',
fieldValidationDefinitions: [ fieldValidationDefinitions: [
@ -64,7 +64,7 @@ describe('addErrorsAndRunHooks', () => {
errorMessage: 'Field is invalid', errorMessage: 'Field is invalid',
}, },
], ],
icon: null, Icon: null,
fieldType: { type: 'input' }, fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.EMAILS, fieldMetadataType: FieldMetadataType.EMAILS,
}; };
@ -88,24 +88,43 @@ describe('addErrorsAndRunHooks', () => {
dataWithoutNameAndInvalidAge, dataWithoutNameAndInvalidAge,
]; ];
const basicError: Info = { message: 'Field is invalid', level: 'error' }; const basicError: SpreadsheetImportInfo = {
const nameError: Info = { message: 'Name Error', level: 'error' }; message: 'Field is invalid',
const ageError: Info = { message: 'Age Error', level: 'error' }; level: 'error',
const regexError: Info = { message: 'Regex error', level: 'error' }; };
const requiredError: Info = { message: 'Field is required', level: 'error' }; const nameError: SpreadsheetImportInfo = {
const duplicatedError: Info = { message: 'Name Error',
level: 'error',
};
const ageError: SpreadsheetImportInfo = {
message: 'Age Error',
level: 'error',
};
const regexError: SpreadsheetImportInfo = {
message: 'Regex error',
level: 'error',
};
const requiredError: SpreadsheetImportInfo = {
message: 'Field is required',
level: 'error',
};
const duplicatedError: SpreadsheetImportInfo = {
message: 'Field must be unique', message: 'Field must be unique',
level: 'error', level: 'error',
}; };
const rowHook: RowHook<'name' | 'age'> = 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: TableHook<'name' | 'age'> = 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(

View File

@ -1,11 +1,11 @@
import { Field } from '@/spreadsheet-import/types'; import { SpreadsheetImportField } from '@/spreadsheet-import/types';
import { findMatch } from '@/spreadsheet-import/utils/findMatch'; import { findMatch } from '@/spreadsheet-import/utils/findMatch';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
describe('findMatch', () => { describe('findMatch', () => {
const defaultField: Field<'defaultField'> = { const defaultField: SpreadsheetImportField<'defaultField'> = {
key: 'defaultField', key: 'defaultField',
icon: null, Icon: null,
label: 'label', label: 'label',
fieldType: { fieldType: {
type: 'input', type: 'input',
@ -14,9 +14,9 @@ describe('findMatch', () => {
alternateMatches: ['Full Name', 'First Name'], alternateMatches: ['Full Name', 'First Name'],
}; };
const secondaryField: Field<'secondaryField'> = { const secondaryField: SpreadsheetImportField<'secondaryField'> = {
key: 'secondaryField', key: 'secondaryField',
icon: null, Icon: null,
label: 'label', label: 'label',
fieldType: { fieldType: {
type: 'input', type: 'input',

View File

@ -1,59 +1,62 @@
import { import {
Column, SpreadsheetImportField,
ColumnType, SpreadsheetImportFieldValidationDefinition,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; } from '@/spreadsheet-import/types';
import { Field, FieldValidationDefinition } from '@/spreadsheet-import/types'; import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
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: Field<'Name'> = { const nameField: SpreadsheetImportField<'Name'> = {
key: 'Name', key: 'Name',
label: 'Name', label: 'Name',
icon: null, Icon: null,
fieldType: { fieldType: {
type: 'input', type: 'input',
}, },
fieldMetadataType: FieldMetadataType.TEXT, fieldMetadataType: FieldMetadataType.TEXT,
}; };
const ageField: Field<'Age'> = { const ageField: SpreadsheetImportField<'Age'> = {
key: 'Age', key: 'Age',
label: 'Age', label: 'Age',
icon: null, Icon: null,
fieldType: { fieldType: {
type: 'input', type: 'input',
}, },
fieldMetadataType: FieldMetadataType.NUMBER, fieldMetadataType: FieldMetadataType.NUMBER,
}; };
const validations: FieldValidationDefinition[] = [{ rule: 'required' }]; const validations: SpreadsheetImportFieldValidationDefinition[] = [
const nameFieldWithValidations: Field<'Name'> = { { rule: 'required' },
];
const nameFieldWithValidations: SpreadsheetImportField<'Name'> = {
...nameField, ...nameField,
fieldValidationDefinitions: validations, fieldValidationDefinitions: validations,
}; };
const ageFieldWithValidations: Field<'Age'> = { const ageFieldWithValidations: SpreadsheetImportField<'Age'> = {
...ageField, ...ageField,
fieldValidationDefinitions: validations, fieldValidationDefinitions: validations,
}; };
type ColumnValues = 'Name' | 'Age'; type ColumnValues = 'Name' | 'Age';
const nameColumn: Column<ColumnValues> = { const nameColumn: SpreadsheetColumn<ColumnValues> = {
type: ColumnType.matched, type: SpreadsheetColumnType.matched,
index: 0, index: 0,
header: '', header: '',
value: 'Name', value: 'Name',
}; };
const ageColumn: Column<ColumnValues> = { const ageColumn: SpreadsheetColumn<ColumnValues> = {
type: ColumnType.matched, type: SpreadsheetColumnType.matched,
index: 0, index: 0,
header: '', header: '',
value: 'Age', value: 'Age',
}; };
const extraColumn: Column<ColumnValues> = { const extraColumn: SpreadsheetColumn<ColumnValues> = {
type: ColumnType.matched, type: SpreadsheetColumnType.matched,
index: 0, index: 0,
header: '', header: '',
value: 'Age', value: 'Age',

View File

@ -1,11 +1,11 @@
import { Field } from '@/spreadsheet-import/types'; import { SpreadsheetImportField } from '@/spreadsheet-import/types';
import { generateExampleRow } from '@/spreadsheet-import/utils/generateExampleRow'; import { generateExampleRow } from '@/spreadsheet-import/utils/generateExampleRow';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
describe('generateExampleRow', () => { describe('generateExampleRow', () => {
const defaultField: Field<'defaultField'> = { const defaultField: SpreadsheetImportField<'defaultField'> = {
key: 'defaultField', key: 'defaultField',
icon: null, Icon: null,
label: 'label', label: 'label',
fieldType: { fieldType: {
type: 'input', type: 'input',
@ -14,7 +14,7 @@ describe('generateExampleRow', () => {
}; };
it('should generate an example row from input field type', () => { it('should generate an example row from input field type', () => {
const fields: Field<'defaultField'>[] = [defaultField]; const fields: SpreadsheetImportField<'defaultField'>[] = [defaultField];
const result = generateExampleRow(fields); const result = generateExampleRow(fields);
@ -22,7 +22,7 @@ describe('generateExampleRow', () => {
}); });
it('should generate an example row from checkbox field type', () => { it('should generate an example row from checkbox field type', () => {
const fields: Field<'defaultField'>[] = [ const fields: SpreadsheetImportField<'defaultField'>[] = [
{ {
...defaultField, ...defaultField,
fieldType: { type: 'checkbox' }, fieldType: { type: 'checkbox' },
@ -36,7 +36,7 @@ describe('generateExampleRow', () => {
}); });
it('should generate an example row from select field type', () => { it('should generate an example row from select field type', () => {
const fields: Field<'defaultField'>[] = [ const fields: SpreadsheetImportField<'defaultField'>[] = [
{ {
...defaultField, ...defaultField,
fieldType: { type: 'select', options: [] }, fieldType: { type: 'select', options: [] },
@ -50,7 +50,7 @@ describe('generateExampleRow', () => {
}); });
it('should generate an example row with provided example values for fields', () => { it('should generate an example row with provided example values for fields', () => {
const fields: Field<'defaultField'>[] = [ const fields: SpreadsheetImportField<'defaultField'>[] = [
{ {
...defaultField, ...defaultField,
example: 'Example', example: 'Example',

View File

@ -1,4 +1,4 @@
import { Field } from '@/spreadsheet-import/types'; import { SpreadsheetImportField } from '@/spreadsheet-import/types';
import { getFieldOptions } from '@/spreadsheet-import/utils/getFieldOptions'; import { getFieldOptions } from '@/spreadsheet-import/utils/getFieldOptions';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
@ -17,10 +17,10 @@ describe('getFieldOptions', () => {
value: 'Three', value: 'Three',
}, },
]; ];
const fields: Field<'Options' | 'Name'>[] = [ const fields: SpreadsheetImportField<'Options' | 'Name'>[] = [
{ {
key: 'Options', key: 'Options',
icon: null, Icon: null,
label: 'options', label: 'options',
fieldType: { fieldType: {
type: 'select', type: 'select',
@ -30,7 +30,7 @@ describe('getFieldOptions', () => {
}, },
{ {
key: 'Name', key: 'Name',
icon: null, Icon: null,
label: 'name', label: 'name',
fieldType: { fieldType: {
type: 'input', type: 'input',

View File

@ -1,49 +1,52 @@
import { import { SpreadsheetImportField } from '@/spreadsheet-import/types';
Column, import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
ColumnType, import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { Field } from '@/spreadsheet-import/types';
import { getMatchedColumns } from '@/spreadsheet-import/utils/getMatchedColumns'; import { getMatchedColumns } from '@/spreadsheet-import/utils/getMatchedColumns';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
describe('getMatchedColumns', () => { describe('getMatchedColumns', () => {
const columns: Column<string>[] = [ const columns: SpreadsheetColumn<string>[] = [
{ index: 0, header: 'Name', type: ColumnType.matched, value: 'Name' }, {
index: 0,
header: 'Name',
type: SpreadsheetColumnType.matched,
value: 'Name',
},
{ {
index: 1, index: 1,
header: 'Location', header: 'Location',
type: ColumnType.matched, type: SpreadsheetColumnType.matched,
value: 'Location', value: 'Location',
}, },
{ {
index: 2, index: 2,
header: 'Age', header: 'Age',
type: ColumnType.matched, type: SpreadsheetColumnType.matched,
value: 'Age', value: 'Age',
}, },
]; ];
const fields: Field<string>[] = [ const fields: SpreadsheetImportField<string>[] = [
{ {
key: 'Name', key: 'Name',
label: 'Name', label: 'Name',
fieldType: { type: 'input' }, fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.TEXT, fieldMetadataType: FieldMetadataType.TEXT,
icon: null, Icon: null,
}, },
{ {
key: 'Location', key: 'Location',
label: 'Location', label: 'Location',
fieldType: { type: 'select', options: [] }, fieldType: { type: 'select', options: [] },
fieldMetadataType: FieldMetadataType.POSITION, fieldMetadataType: FieldMetadataType.POSITION,
icon: null, Icon: null,
}, },
{ {
key: 'Age', key: 'Age',
label: 'Age', label: 'Age',
fieldType: { type: 'input' }, fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.NUMBER, fieldMetadataType: FieldMetadataType.NUMBER,
icon: null, Icon: null,
}, },
]; ];
@ -57,11 +60,16 @@ describe('getMatchedColumns', () => {
it('should return matched columns for each field', () => { it('should return matched columns for each field', () => {
const result = getMatchedColumns(columns, fields, data, autoMapDistance); const result = getMatchedColumns(columns, fields, data, autoMapDistance);
expect(result).toEqual([ expect(result).toEqual([
{ index: 0, header: 'Name', type: ColumnType.matched, value: 'Name' }, {
index: 0,
header: 'Name',
type: SpreadsheetColumnType.matched,
value: 'Name',
},
{ {
index: 1, index: 1,
header: 'Location', header: 'Location',
type: ColumnType.matchedSelect, type: SpreadsheetColumnType.matchedSelect,
value: 'Location', value: 'Location',
matchedOptions: [ matchedOptions: [
{ {
@ -72,18 +80,33 @@ describe('getMatchedColumns', () => {
}, },
], ],
}, },
{ index: 2, header: 'Age', type: ColumnType.matched, value: 'Age' }, {
index: 2,
header: 'Age',
type: SpreadsheetColumnType.matched,
value: 'Age',
},
]); ]);
}); });
it('should handle columns with duplicate values by choosing the closest match', () => { it('should handle columns with duplicate values by choosing the closest match', () => {
const columnsWithDuplicates: Column<string>[] = [ const columnsWithDuplicates: SpreadsheetColumn<string>[] = [
{ index: 0, header: 'Name', type: ColumnType.matched, value: 'Name' }, {
{ index: 1, header: 'Name', type: ColumnType.matched, value: 'Name' }, index: 0,
header: 'Name',
type: SpreadsheetColumnType.matched,
value: 'Name',
},
{
index: 1,
header: 'Name',
type: SpreadsheetColumnType.matched,
value: 'Name',
},
{ {
index: 2, index: 2,
header: 'Location', header: 'Location',
type: ColumnType.matched, type: SpreadsheetColumnType.matched,
value: 'Location', value: 'Location',
}, },
]; ];
@ -98,12 +121,12 @@ describe('getMatchedColumns', () => {
expect(result[0]).toEqual({ expect(result[0]).toEqual({
index: 0, index: 0,
header: 'Name', header: 'Name',
type: ColumnType.empty, type: SpreadsheetColumnType.empty,
}); });
expect(result[1]).toEqual({ expect(result[1]).toEqual({
index: 1, index: 1,
header: 'Name', header: 'Name',
type: ColumnType.matched, type: SpreadsheetColumnType.matched,
value: 'Name', value: 'Name',
}); });
}); });
@ -114,20 +137,20 @@ describe('getMatchedColumns', () => {
['Alice', 'Los Angeles', '25'], ['Alice', 'Los Angeles', '25'],
]; ];
const unmatchedFields: Field<string>[] = [ const unmatchedFields: SpreadsheetImportField<string>[] = [
{ {
key: 'Hobby', key: 'Hobby',
label: 'Hobby', label: 'Hobby',
fieldType: { type: 'input' }, fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.TEXT, fieldMetadataType: FieldMetadataType.TEXT,
icon: null, Icon: null,
}, },
{ {
key: 'Interest', key: 'Interest',
label: 'Interest', label: 'Interest',
fieldType: { type: 'input' }, fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.TEXT, fieldMetadataType: FieldMetadataType.TEXT,
icon: null, Icon: null,
}, },
]; ];

View File

@ -1,37 +1,46 @@
import { import { SpreadsheetImportField } from '@/spreadsheet-import/types';
Column, import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
ColumnType, import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { Field } from '@/spreadsheet-import/types';
import { normalizeTableData } from '@/spreadsheet-import/utils/normalizeTableData'; import { normalizeTableData } from '@/spreadsheet-import/utils/normalizeTableData';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
describe('normalizeTableData', () => { describe('normalizeTableData', () => {
const columns: Column<string>[] = [ const columns: SpreadsheetColumn<string>[] = [
{ index: 0, header: 'Name', type: ColumnType.matched, value: 'name' }, {
{ index: 1, header: 'Age', type: ColumnType.matched, value: 'age' }, index: 0,
header: 'Name',
type: SpreadsheetColumnType.matched,
value: 'name',
},
{
index: 1,
header: 'Age',
type: SpreadsheetColumnType.matched,
value: 'age',
},
{ {
index: 2, index: 2,
header: 'Active', header: 'Active',
type: ColumnType.matchedCheckbox, type: SpreadsheetColumnType.matchedCheckbox,
value: 'active', value: 'active',
}, },
]; ];
const fields: Field<string>[] = [ const fields: SpreadsheetImportField<string>[] = [
{ {
key: 'name', key: 'name',
label: 'Name', label: 'Name',
fieldType: { type: 'input' }, fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.TEXT, fieldMetadataType: FieldMetadataType.TEXT,
icon: null, Icon: null,
}, },
{ {
key: 'age', key: 'age',
label: 'Age', label: 'Age',
fieldType: { type: 'input' }, fieldType: { type: 'input' },
fieldMetadataType: FieldMetadataType.NUMBER, fieldMetadataType: FieldMetadataType.NUMBER,
icon: null, Icon: null,
}, },
{ {
key: 'active', key: 'active',
@ -40,7 +49,7 @@ describe('normalizeTableData', () => {
type: 'checkbox', type: 'checkbox',
}, },
fieldMetadataType: FieldMetadataType.BOOLEAN, fieldMetadataType: FieldMetadataType.BOOLEAN,
icon: null, Icon: null,
}, },
]; ];
@ -61,16 +70,16 @@ describe('normalizeTableData', () => {
}); });
it('should normalize matchedCheckbox values and handle booleanMatches', () => { it('should normalize matchedCheckbox values and handle booleanMatches', () => {
const columns: Column<string>[] = [ const columns: SpreadsheetColumn<string>[] = [
{ {
index: 0, index: 0,
header: 'Active', header: 'Active',
type: ColumnType.matchedCheckbox, type: SpreadsheetColumnType.matchedCheckbox,
value: 'active', value: 'active',
}, },
]; ];
const fields: Field<string>[] = [ const fields: SpreadsheetImportField<string>[] = [
{ {
key: 'active', key: 'active',
label: 'Active', label: 'Active',
@ -79,7 +88,7 @@ describe('normalizeTableData', () => {
booleanMatches: { yes: true, no: false }, booleanMatches: { yes: true, no: false },
}, },
fieldMetadataType: FieldMetadataType.BOOLEAN, fieldMetadataType: FieldMetadataType.BOOLEAN,
icon: null, Icon: null,
}, },
]; ];
@ -91,11 +100,11 @@ describe('normalizeTableData', () => {
}); });
it('should map matchedSelect and matchedSelectOptions values correctly', () => { it('should map matchedSelect and matchedSelectOptions values correctly', () => {
const columns: Column<string>[] = [ const columns: SpreadsheetColumn<string>[] = [
{ {
index: 0, index: 0,
header: 'Number', header: 'Number',
type: ColumnType.matchedSelect, type: SpreadsheetColumnType.matchedSelect,
value: 'number', value: 'number',
matchedOptions: [ matchedOptions: [
{ entry: 'One', value: '1' }, { entry: 'One', value: '1' },
@ -104,7 +113,7 @@ describe('normalizeTableData', () => {
}, },
]; ];
const fields: Field<string>[] = [ const fields: SpreadsheetImportField<string>[] = [
{ {
key: 'number', key: 'number',
label: 'Number', label: 'Number',
@ -116,7 +125,7 @@ describe('normalizeTableData', () => {
], ],
}, },
fieldMetadataType: FieldMetadataType.SELECT, fieldMetadataType: FieldMetadataType.SELECT,
icon: null, Icon: null,
}, },
]; ];
@ -132,9 +141,9 @@ describe('normalizeTableData', () => {
}); });
it('should handle empty and ignored columns', () => { it('should handle empty and ignored columns', () => {
const columns: Column<string>[] = [ const columns: SpreadsheetColumn<string>[] = [
{ index: 0, header: 'Empty', type: ColumnType.empty }, { index: 0, header: 'Empty', type: SpreadsheetColumnType.empty },
{ index: 1, header: 'Ignored', type: ColumnType.ignored }, { index: 1, header: 'Ignored', type: SpreadsheetColumnType.ignored },
]; ];
const rawData = [['Value1', 'Value2']]; const rawData = [['Value1', 'Value2']];
@ -145,11 +154,11 @@ describe('normalizeTableData', () => {
}); });
it('should handle unrecognized column types and return empty object', () => { it('should handle unrecognized column types and return empty object', () => {
const columns: Column<string>[] = [ const columns: SpreadsheetColumns<string> = [
{ {
index: 0, index: 0,
header: 'Unrecognized', header: 'Unrecognized',
type: 'Unknown' as unknown as ColumnType.matched, type: 'Unknown' as unknown as SpreadsheetColumnType.matched,
value: '', value: '',
}, },
]; ];

View File

@ -1,24 +1,22 @@
import { import { SpreadsheetImportField } from '@/spreadsheet-import/types';
Column, import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
ColumnType, import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { Field } from '@/spreadsheet-import/types';
import { setColumn } from '@/spreadsheet-import/utils/setColumn'; 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: Field<'Name'> = { 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,
}; };
const oldColumn: Column<'oldValue'> = { const oldColumn: SpreadsheetColumn<'oldValue'> = {
index: 0, index: 0,
header: 'Name', header: 'Name',
type: ColumnType.matched, type: SpreadsheetColumnType.matched,
value: 'oldValue', value: 'oldValue',
}; };
@ -29,7 +27,7 @@ describe('setColumn', () => {
type: 'select', type: 'select',
options: [{ value: 'John' }, { value: 'Alice' }], options: [{ value: 'John' }, { value: 'Alice' }],
}, },
} as Field<'Name'>; } as SpreadsheetImportField<'Name'>;
const data = [['John'], ['Alice']]; const data = [['John'], ['Alice']];
const result = setColumn(oldColumn, field, data); const result = setColumn(oldColumn, field, data);
@ -37,7 +35,7 @@ describe('setColumn', () => {
expect(result).toEqual({ expect(result).toEqual({
index: 0, index: 0,
header: 'Name', header: 'Name',
type: ColumnType.matchedSelectOptions, type: SpreadsheetColumnType.matchedSelectOptions,
value: 'Name', value: 'Name',
matchedOptions: [ matchedOptions: [
{ {
@ -56,14 +54,14 @@ describe('setColumn', () => {
const field = { const field = {
...defaultField, ...defaultField,
fieldType: { type: 'checkbox' }, fieldType: { type: 'checkbox' },
} as Field<'Name'>; } as SpreadsheetImportField<'Name'>;
const result = setColumn(oldColumn, field); const result = setColumn(oldColumn, field);
expect(result).toEqual({ expect(result).toEqual({
index: 0, index: 0,
header: 'Name', header: 'Name',
type: ColumnType.matchedCheckbox, type: SpreadsheetColumnType.matchedCheckbox,
value: 'Name', value: 'Name',
}); });
}); });
@ -72,14 +70,14 @@ describe('setColumn', () => {
const field = { const field = {
...defaultField, ...defaultField,
fieldType: { type: 'input' }, fieldType: { type: 'input' },
} as Field<'Name'>; } as SpreadsheetImportField<'Name'>;
const result = setColumn(oldColumn, field); const result = setColumn(oldColumn, field);
expect(result).toEqual({ expect(result).toEqual({
index: 0, index: 0,
header: 'Name', header: 'Name',
type: ColumnType.matched, type: SpreadsheetColumnType.matched,
value: 'Name', value: 'Name',
}); });
}); });
@ -88,14 +86,14 @@ describe('setColumn', () => {
const field = { const field = {
...defaultField, ...defaultField,
fieldType: { type: 'unknown' }, fieldType: { type: 'unknown' },
} as unknown as Field<'Name'>; } as unknown as SpreadsheetImportField<'Name'>;
const result = setColumn(oldColumn, field); const result = setColumn(oldColumn, field);
expect(result).toEqual({ expect(result).toEqual({
index: 0, index: 0,
header: 'Name', header: 'Name',
type: ColumnType.empty, type: SpreadsheetColumnType.empty,
}); });
}); });
}); });

View File

@ -1,22 +1,20 @@
import { import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
Column, import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
ColumnType,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { setIgnoreColumn } from '@/spreadsheet-import/utils/setIgnoreColumn'; 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: Column<'John'> = { const column: SpreadsheetColumn<'John'> = {
index: 0, index: 0,
header: 'Name', header: 'Name',
type: ColumnType.matched, type: SpreadsheetColumnType.matched,
value: 'John', value: 'John',
}; };
const result = setIgnoreColumn(column); const result = setIgnoreColumn(column);
expect(result).toEqual({ expect(result).toEqual({
index: 0, index: 0,
header: 'Name', header: 'Name',
type: ColumnType.ignored, type: SpreadsheetColumnType.ignored,
}); });
}); });
}); });

View File

@ -1,15 +1,13 @@
import { import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
Column, import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
ColumnType,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { setSubColumn } from '@/spreadsheet-import/utils/setSubColumn'; 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: Column<'John' | ''> = { const oldColumn: SpreadsheetColumn<'John' | ''> = {
index: 0, index: 0,
header: 'Name', header: 'Name',
type: ColumnType.matchedSelect, type: SpreadsheetColumnType.matchedSelect,
matchedOptions: [ matchedOptions: [
{ entry: 'Name1', value: 'John' }, { entry: 'Name1', value: 'John' },
{ entry: 'Name2', value: '' }, { entry: 'Name2', value: '' },
@ -24,7 +22,7 @@ describe('setSubColumn', () => {
expect(result).toEqual({ expect(result).toEqual({
index: 0, index: 0,
header: 'Name', header: 'Name',
type: ColumnType.matchedSelect, type: SpreadsheetColumnType.matchedSelect,
matchedOptions: [ matchedOptions: [
{ entry: 'Name1', value: 'John Doe' }, { entry: 'Name1', value: 'John Doe' },
{ entry: 'Name2', value: '' }, { entry: 'Name2', value: '' },
@ -34,10 +32,10 @@ describe('setSubColumn', () => {
}); });
it('should return a matchedSelectOptionsColumn with updated matchedOptions', () => { it('should return a matchedSelectOptionsColumn with updated matchedOptions', () => {
const oldColumn: Column<'John' | 'Jane'> = { const oldColumn: SpreadsheetColumn<'John' | 'Jane'> = {
index: 0, index: 0,
header: 'Name', header: 'Name',
type: ColumnType.matchedSelectOptions, type: SpreadsheetColumnType.matchedSelectOptions,
matchedOptions: [ matchedOptions: [
{ entry: 'Name1', value: 'John' }, { entry: 'Name1', value: 'John' },
{ entry: 'Name2', value: 'Jane' }, { entry: 'Name2', value: 'Jane' },
@ -52,7 +50,7 @@ describe('setSubColumn', () => {
expect(result).toEqual({ expect(result).toEqual({
index: 0, index: 0,
header: 'Name', header: 'Name',
type: ColumnType.matchedSelectOptions, type: SpreadsheetColumnType.matchedSelectOptions,
matchedOptions: [ matchedOptions: [
{ entry: 'Name1', value: 'John Doe' }, { entry: 'Name1', value: 'John Doe' },
{ entry: 'Name2', value: 'Jane' }, { entry: 'Name2', value: 'Jane' },

View File

@ -6,24 +6,28 @@ import {
ImportedStructuredRowMetadata, ImportedStructuredRowMetadata,
} from '@/spreadsheet-import/steps/components/ValidationStep/types'; } from '@/spreadsheet-import/steps/components/ValidationStep/types';
import { import {
Fields,
ImportedStructuredRow, ImportedStructuredRow,
Info, SpreadsheetImportFields,
RowHook, SpreadsheetImportInfo,
TableHook, SpreadsheetImportRowHook,
SpreadsheetImportTableHook,
} from '@/spreadsheet-import/types'; } from '@/spreadsheet-import/types';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const addErrorsAndRunHooks = <T extends string>( export const addErrorsAndRunHooks = <T extends string>(
data: (ImportedStructuredRow<T> & Partial<ImportedStructuredRowMetadata>)[], data: (ImportedStructuredRow<T> & Partial<ImportedStructuredRowMetadata>)[],
fields: Fields<T>, fields: SpreadsheetImportFields<T>,
rowHook?: RowHook<T>, rowHook?: SpreadsheetImportRowHook<T>,
tableHook?: TableHook<T>, tableHook?: SpreadsheetImportTableHook<T>,
): (ImportedStructuredRow<T> & ImportedStructuredRowMetadata)[] => { ): (ImportedStructuredRow<T> & ImportedStructuredRowMetadata)[] => {
const errors: Errors = {}; const errors: Errors = {};
const addHookError = (rowIndex: number, fieldKey: T, error: Info) => { const addHookError = (
rowIndex: number,
fieldKey: T,
error: SpreadsheetImportInfo,
) => {
errors[rowIndex] = { errors[rowIndex] = {
...errors[rowIndex], ...errors[rowIndex],
[fieldKey]: error, [fieldKey]: error,

View File

@ -1,7 +1,6 @@
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
import lavenstein from 'js-levenshtein'; import lavenstein from 'js-levenshtein';
import { Fields } from '@/spreadsheet-import/types';
type AutoMatchAccumulator<T> = { type AutoMatchAccumulator<T> = {
distance: number; distance: number;
value: T; value: T;
@ -9,7 +8,7 @@ type AutoMatchAccumulator<T> = {
export const findMatch = <T extends string>( export const findMatch = <T extends string>(
header: string, header: string,
fields: Fields<T>, fields: SpreadsheetImportFields<T>,
autoMapDistance: number, autoMapDistance: number,
): T | undefined => { ): T | undefined => {
const smallestValue = fields.reduce<AutoMatchAccumulator<T>>((acc, field) => { const smallestValue = fields.reduce<AutoMatchAccumulator<T>>((acc, field) => {

View File

@ -1,9 +1,9 @@
import { Columns } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
import { Fields } from '@/spreadsheet-import/types'; import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
export const findUnmatchedRequiredFields = <T extends string>( export const findUnmatchedRequiredFields = <T extends string>(
fields: Fields<T>, fields: SpreadsheetImportFields<T>,
columns: Columns<T>, columns: SpreadsheetColumns<T>,
) => ) =>
fields fields
.filter((field) => .filter((field) =>

View File

@ -1,13 +1,21 @@
import { Field, Fields } from '@/spreadsheet-import/types'; import {
SpreadsheetImportField,
SpreadsheetImportFields,
} from '@/spreadsheet-import/types';
const titleMap: Record<Field<string>['fieldType']['type'], string> = { const titleMap: Record<
SpreadsheetImportField<string>['fieldType']['type'],
string
> = {
checkbox: 'Boolean', checkbox: 'Boolean',
select: 'Options', select: 'Options',
multiSelect: 'Options', multiSelect: 'Options',
input: 'Text', input: 'Text',
}; };
export const generateExampleRow = <T extends string>(fields: Fields<T>) => [ export const generateExampleRow = <T extends string>(
fields: SpreadsheetImportFields<T>,
) => [
fields.reduce( fields.reduce(
(acc, field) => { (acc, field) => {
acc[field.key as T] = field.example || titleMap[field.fieldType.type]; acc[field.key as T] = field.example || titleMap[field.fieldType.type];

View File

@ -1,7 +1,7 @@
import { Fields } from '@/spreadsheet-import/types'; import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
export const getFieldOptions = <T extends string>( export const getFieldOptions = <T extends string>(
fields: Fields<T>, fields: SpreadsheetImportFields<T>,
fieldKey: string, fieldKey: string,
) => { ) => {
const field = fields.find(({ key }) => fieldKey === key); const field = fields.find(({ key }) => fieldKey === key);

View File

@ -1,26 +1,29 @@
import lavenstein from 'js-levenshtein'; import lavenstein from 'js-levenshtein';
import { import { MatchColumnsStepProps } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
Column,
Columns,
MatchColumnsStepProps,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { Field, Fields } from '@/spreadsheet-import/types';
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 { findMatch } from './findMatch';
import { setColumn } from './setColumn'; import { setColumn } from './setColumn';
import { isDefined } from 'twenty-shared/utils';
export const getMatchedColumns = <T extends string>( export const getMatchedColumns = <T extends string>(
columns: Columns<T>, columns: SpreadsheetColumns<T>,
fields: Fields<T>, fields: SpreadsheetImportFields<T>,
data: MatchColumnsStepProps['data'], data: MatchColumnsStepProps['data'],
autoMapDistance: number, autoMapDistance: number,
) => ) =>
columns.reduce<Column<T>[]>((arr, column) => { columns.reduce<SpreadsheetColumn<T>[]>((arr, column) => {
const autoMatch = findMatch(column.header, fields, autoMapDistance); const autoMatch = findMatch(column.header, fields, autoMapDistance);
if (isDefined(autoMatch)) { if (isDefined(autoMatch)) {
const field = fields.find((field) => field.key === autoMatch) as Field<T>; const field = fields.find(
(field) => field.key === autoMatch,
) as SpreadsheetImportField<T>;
const duplicateIndex = arr.findIndex( const duplicateIndex = arr.findIndex(
(column) => 'value' in column && column.value === field.key, (column) => 'value' in column && column.value === field.key,
); );

View File

@ -1,26 +1,24 @@
import { import {
Columns,
ColumnType,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import {
Fields,
ImportedRow, ImportedRow,
ImportedStructuredRow, ImportedStructuredRow,
SpreadsheetImportFields,
} from '@/spreadsheet-import/types'; } from '@/spreadsheet-import/types';
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { isDefined } from 'twenty-shared/utils';
import { z } from 'zod'; import { z } from 'zod';
import { normalizeCheckboxValue } from './normalizeCheckboxValue'; import { normalizeCheckboxValue } from './normalizeCheckboxValue';
import { isDefined } from 'twenty-shared/utils';
export const normalizeTableData = <T extends string>( export const normalizeTableData = <T extends string>(
columns: Columns<T>, columns: SpreadsheetColumns<T>,
data: ImportedRow[], data: ImportedRow[],
fields: Fields<T>, fields: SpreadsheetImportFields<T>,
) => ) =>
data.map((row) => data.map((row) =>
columns.reduce((acc, column, index) => { columns.reduce((acc, column, index) => {
const curr = row[index]; const curr = row[index];
switch (column.type) { switch (column.type) {
case ColumnType.matchedCheckbox: { case SpreadsheetColumnType.matchedCheckbox: {
const field = fields.find((field) => field.key === column.value); const field = fields.find((field) => field.key === column.value);
if (!field) { if (!field) {
@ -49,12 +47,12 @@ export const normalizeTableData = <T extends string>(
} }
return acc; return acc;
} }
case ColumnType.matched: { case SpreadsheetColumnType.matched: {
acc[column.value] = curr === '' ? undefined : curr; acc[column.value] = curr === '' ? undefined : curr;
return acc; return acc;
} }
case ColumnType.matchedSelect: case SpreadsheetColumnType.matchedSelect:
case ColumnType.matchedSelectOptions: { case SpreadsheetColumnType.matchedSelectOptions: {
const field = fields.find((field) => field.key === column.value); const field = fields.find((field) => field.key === column.value);
if (!field) { if (!field) {
@ -96,8 +94,8 @@ export const normalizeTableData = <T extends string>(
} }
return acc; return acc;
} }
case ColumnType.empty: case SpreadsheetColumnType.empty:
case ColumnType.ignored: { case SpreadsheetColumnType.ignored: {
return acc; return acc;
} }
default: default:

View File

@ -1,25 +1,23 @@
import { import { MatchColumnsStepProps } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
Column,
ColumnType,
MatchColumnsStepProps,
MatchedOptions,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { Field } from '@/spreadsheet-import/types';
import { SpreadsheetImportField } from '@/spreadsheet-import/types';
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
import { z } from 'zod'; import { z } from 'zod';
import { uniqueEntries } from './uniqueEntries'; import { uniqueEntries } from './uniqueEntries';
export const setColumn = <T extends string>( export const setColumn = <T extends string>(
oldColumn: Column<T>, oldColumn: SpreadsheetColumn<T>,
field?: Field<T>, field?: SpreadsheetImportField<T>,
data?: MatchColumnsStepProps['data'], data?: MatchColumnsStepProps['data'],
): Column<T> => { ): 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 MatchedOptions<T>[]; ) as SpreadsheetMatchedOptions<T>[];
const matchedOptions = uniqueData.map((record) => { const matchedOptions = uniqueData.map((record) => {
const value = fieldOptions.find( const value = fieldOptions.find(
@ -28,8 +26,8 @@ export const setColumn = <T extends string>(
fieldOption.label === record.entry, fieldOption.label === record.entry,
)?.value; )?.value;
return value return value
? ({ ...record, value } as MatchedOptions<T>) ? ({ ...record, value } as SpreadsheetMatchedOptions<T>)
: (record as MatchedOptions<T>); : (record as SpreadsheetMatchedOptions<T>);
}); });
const allMatched = const allMatched =
matchedOptions.filter((o) => o.value).length === uniqueData?.length; matchedOptions.filter((o) => o.value).length === uniqueData?.length;
@ -37,8 +35,8 @@ export const setColumn = <T extends string>(
return { return {
...oldColumn, ...oldColumn,
type: allMatched type: allMatched
? ColumnType.matchedSelectOptions ? SpreadsheetColumnType.matchedSelectOptions
: ColumnType.matchedSelect, : SpreadsheetColumnType.matchedSelect,
value: field.key, value: field.key,
matchedOptions, matchedOptions,
}; };
@ -69,8 +67,8 @@ export const setColumn = <T extends string>(
fieldOption.value === entry || fieldOption.label === entry, fieldOption.value === entry || fieldOption.label === entry,
)?.value; )?.value;
return value return value
? ({ entry, value } as MatchedOptions<T>) ? ({ entry, value } as SpreadsheetMatchedOptions<T>)
: ({ entry } as MatchedOptions<T>); : ({ entry } as SpreadsheetMatchedOptions<T>);
}); });
const areAllMatched = const areAllMatched =
matchedOptions.filter((option) => option.value).length === matchedOptions.filter((option) => option.value).length ===
@ -79,8 +77,8 @@ export const setColumn = <T extends string>(
return { return {
...oldColumn, ...oldColumn,
type: areAllMatched type: areAllMatched
? ColumnType.matchedSelectOptions ? SpreadsheetColumnType.matchedSelectOptions
: ColumnType.matchedSelect, : SpreadsheetColumnType.matchedSelect,
value: field.key, value: field.key,
matchedOptions, matchedOptions,
}; };
@ -89,7 +87,7 @@ export const setColumn = <T extends string>(
if (field?.fieldType.type === 'checkbox') { if (field?.fieldType.type === 'checkbox') {
return { return {
index: oldColumn.index, index: oldColumn.index,
type: ColumnType.matchedCheckbox, type: SpreadsheetColumnType.matchedCheckbox,
value: field.key, value: field.key,
header: oldColumn.header, header: oldColumn.header,
}; };
@ -98,7 +96,7 @@ export const setColumn = <T extends string>(
if (field?.fieldType.type === 'input') { if (field?.fieldType.type === 'input') {
return { return {
index: oldColumn.index, index: oldColumn.index,
type: ColumnType.matched, type: SpreadsheetColumnType.matched,
value: field.key, value: field.key,
header: oldColumn.header, header: oldColumn.header,
}; };
@ -107,6 +105,6 @@ export const setColumn = <T extends string>(
return { return {
index: oldColumn.index, index: oldColumn.index,
header: oldColumn.header, header: oldColumn.header,
type: ColumnType.empty, type: SpreadsheetColumnType.empty,
}; };
}; };

View File

@ -1,13 +1,11 @@
import { import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
Column, import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
ColumnType,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
export const setIgnoreColumn = <T extends string>({ export const setIgnoreColumn = <T extends string>({
header, header,
index, index,
}: Column<T>): Column<T> => ({ }: SpreadsheetColumn<T>): SpreadsheetColumn<T> => ({
header, header,
index, index,
type: ColumnType.ignored, type: SpreadsheetColumnType.ignored,
}); });

View File

@ -1,30 +1,34 @@
import { import {
ColumnType, SpreadsheetMatchedSelectColumn,
MatchedOptions, SpreadsheetMatchedSelectOptionsColumn,
MatchedSelectColumn, } from '@/spreadsheet-import/types/SpreadsheetColumn';
MatchedSelectOptionsColumn, import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
export const setSubColumn = <T>( export const setSubColumn = <T>(
oldColumn: MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T>, oldColumn:
| SpreadsheetMatchedSelectColumn<T>
| SpreadsheetMatchedSelectOptionsColumn<T>,
entry: string, entry: string,
value: string, value: string,
): MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T> => { ):
| SpreadsheetMatchedSelectColumn<T>
| SpreadsheetMatchedSelectOptionsColumn<T> => {
const options = oldColumn.matchedOptions.map((option) => const options = oldColumn.matchedOptions.map((option) =>
option.entry === entry ? { ...option, value } : option, option.entry === entry ? { ...option, value } : option,
); );
const allMathced = options.every(({ value }) => !!value); const allMatched = options.every(({ value }) => !!value);
if (allMathced) { if (allMatched) {
return { return {
...oldColumn, ...oldColumn,
matchedOptions: options as MatchedOptions<T>[], matchedOptions: options as SpreadsheetMatchedOptions<T>[],
type: ColumnType.matchedSelectOptions, type: SpreadsheetColumnType.matchedSelectOptions,
}; };
} else { } else {
return { return {
...oldColumn, ...oldColumn,
matchedOptions: options as MatchedOptions<T>[], matchedOptions: options as SpreadsheetMatchedOptions<T>[],
type: ColumnType.matchedSelect, type: SpreadsheetColumnType.matchedSelect,
}; };
} }
}; };

View File

@ -1,14 +1,12 @@
import uniqBy from 'lodash.uniqby'; import uniqBy from 'lodash.uniqby';
import { import { MatchColumnsStepProps } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
MatchColumnsStepProps, import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
MatchedOptions,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
export const uniqueEntries = <T extends string>( export const uniqueEntries = <T extends string>(
data: MatchColumnsStepProps['data'], data: MatchColumnsStepProps['data'],
index: number, index: number,
): Partial<MatchedOptions<T>>[] => ): Partial<SpreadsheetMatchedOptions<T>>[] =>
uniqBy( uniqBy(
data.map((row) => ({ entry: row[index] })), data.map((row) => ({ entry: row[index] })),
'entry', 'entry',

View File

@ -1,9 +1,7 @@
import { Tag, THEME_COMMON } from 'twenty-ui'; import { SelectOption, Tag, THEME_COMMON } from 'twenty-ui';
import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata';
import { SelectOption } from '@/spreadsheet-import/types';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
const spacing1 = THEME_COMMON.spacing(1); const spacing1 = THEME_COMMON.spacing(1);
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -40,7 +38,7 @@ export const MultiSelectDisplay = ({
key={index} key={index}
color={selectedOption.color ?? 'transparent'} color={selectedOption.color ?? 'transparent'}
text={selectedOption.label} text={selectedOption.label}
Icon={selectedOption.icon ?? undefined} Icon={selectedOption.Icon ?? undefined}
/> />
))} ))}
</StyledContainer> </StyledContainer>

View File

@ -3,7 +3,6 @@ import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata';
import { SelectOption } from '@/spreadsheet-import/types';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
@ -13,9 +12,9 @@ import { useSelectableListStates } from '@/ui/layout/selectable-list/hooks/inter
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { MenuItemMultiSelectTag } from 'twenty-ui';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { MenuItemMultiSelectTag, SelectOption } from 'twenty-ui';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
type MultiSelectInputProps = { type MultiSelectInputProps = {
selectableListComponentInstanceId: string; selectableListComponentInstanceId: string;
@ -128,7 +127,7 @@ export const MultiSelectInput = ({
selected={values?.includes(option.value) || false} selected={values?.includes(option.value) || false}
text={option.label} text={option.label}
color={option.color ?? 'transparent'} color={option.color ?? 'transparent'}
Icon={option.icon ?? undefined} Icon={option.Icon ?? undefined}
onClick={() => onClick={() =>
onOptionSelected(formatNewSelectedOptions(option.value)) onOptionSelected(formatNewSelectedOptions(option.value))
} }

View File

@ -1,6 +1,6 @@
import { SelectOption } from '@/spreadsheet-import/types';
import { SelectInput as SelectBaseInput } from '@/ui/input/components/SelectInput'; import { SelectInput as SelectBaseInput } from '@/ui/input/components/SelectInput';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectOption } from 'twenty-ui';
type SelectInputProps = { type SelectInputProps = {
selectableListComponentInstanceId: string; selectableListComponentInstanceId: string;

View File

@ -1,6 +1,11 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { MouseEvent, useMemo, useRef, useState } from 'react'; import { MouseEvent, useMemo, useRef, useState } from 'react';
import { IconComponent, MenuItem, MenuItemSelect } from 'twenty-ui'; import {
IconComponent,
MenuItem,
MenuItemSelect,
SelectOption,
} from 'twenty-ui';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@ -9,14 +14,8 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { SelectControl } from '@/ui/input/components/SelectControl'; import { SelectControl } from '@/ui/input/components/SelectControl';
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
export type SelectOption<Value extends string | number | boolean | null> = {
value: Value;
label: string;
Icon?: IconComponent;
};
export type SelectSizeVariant = 'small' | 'default'; export type SelectSizeVariant = 'small' | 'default';

View File

@ -1,8 +1,12 @@
import { SelectOption, SelectSizeVariant } from '@/ui/input/components/Select'; import { SelectSizeVariant } from '@/ui/input/components/Select';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { IconChevronDown, OverflowingTextWithTooltip } from 'twenty-ui';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import {
IconChevronDown,
OverflowingTextWithTooltip,
SelectOption,
} from 'twenty-ui';
const StyledControlContainer = styled.div<{ const StyledControlContainer = styled.div<{
disabled?: boolean; disabled?: boolean;

View File

@ -1,5 +1,3 @@
import { SelectOption } from '@/spreadsheet-import/types';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
@ -8,8 +6,8 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { MenuItemSelectTag, TagColor } from 'twenty-ui';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { MenuItemSelectTag, SelectOption, TagColor } from 'twenty-ui';
interface SelectInputProps { interface SelectInputProps {
onOptionSelected: (selectedOption: SelectOption) => void; onOptionSelected: (selectedOption: SelectOption) => void;
@ -125,7 +123,7 @@ export const SelectInput = ({
text={option.label} text={option.label}
color={(option.color as TagColor) ?? 'transparent'} color={(option.color as TagColor) ?? 'transparent'}
onClick={() => handleOptionChange(option)} onClick={() => handleOptionChange(option)}
LeftIcon={option.icon} LeftIcon={option.Icon}
/> />
); );
})} })}

View File

@ -1,9 +1,9 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { IconCircleOff, IconComponentProps } from 'twenty-ui'; import { IconCircleOff, IconComponentProps, SelectOption } from 'twenty-ui';
import { SELECT_COUNTRY_DROPDOWN_ID } from '@/ui/input/components/internal/country/constants/SelectCountryDropdownId'; import { SELECT_COUNTRY_DROPDOWN_ID } from '@/ui/input/components/internal/country/constants/SelectCountryDropdownId';
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries'; import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
import { Select, SelectOption } from '@/ui/input/components/Select'; import { Select } from '@/ui/input/components/Select';
export const CountrySelect = ({ export const CountrySelect = ({
label, label,

View File

@ -2,7 +2,7 @@ import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilte
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition'; import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput'; import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput';
import { Select, SelectOption } from '@/ui/input/components/Select'; import { Select } from '@/ui/input/components/Select';
import { useViewOrDefaultViewFromPrefetchedViews } from '@/views/hooks/useViewOrDefaultViewFromPrefetchedViews'; import { useViewOrDefaultViewFromPrefetchedViews } from '@/views/hooks/useViewOrDefaultViewFromPrefetchedViews';
import { WorkflowCreateRecordAction } from '@/workflow/types/Workflow'; import { WorkflowCreateRecordAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
@ -12,11 +12,11 @@ import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-ac
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { HorizontalSeparator, useIcons } from 'twenty-ui'; import { isDefined } from 'twenty-shared/utils';
import { HorizontalSeparator, SelectOption, useIcons } from 'twenty-ui';
import { JsonValue } from 'type-fest'; import { JsonValue } from 'type-fest';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import { FieldMetadataType } from '~/generated/graphql'; import { FieldMetadataType } from '~/generated/graphql';
import { isDefined } from 'twenty-shared/utils';
type WorkflowEditActionCreateRecordProps = { type WorkflowEditActionCreateRecordProps = {
action: WorkflowCreateRecordAction; action: WorkflowCreateRecordAction;

View File

@ -1,5 +1,5 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { Select, SelectOption } from '@/ui/input/components/Select'; import { Select } from '@/ui/input/components/Select';
import { WorkflowDeleteRecordAction } from '@/workflow/types/Workflow'; import { WorkflowDeleteRecordAction } from '@/workflow/types/Workflow';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader'; import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { WorkflowSingleRecordPicker } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowSingleRecordPicker'; import { WorkflowSingleRecordPicker } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowSingleRecordPicker';
@ -9,10 +9,10 @@ import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowS
import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow'; import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow';
import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow'; import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { HorizontalSeparator, useIcons } from 'twenty-ui'; import { isDefined } from 'twenty-shared/utils';
import { HorizontalSeparator, SelectOption, useIcons } from 'twenty-ui';
import { JsonValue } from 'type-fest'; import { JsonValue } from 'type-fest';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import { isDefined } from 'twenty-shared/utils';
type WorkflowEditActionDeleteRecordProps = { type WorkflowEditActionDeleteRecordProps = {
action: WorkflowDeleteRecordAction; action: WorkflowDeleteRecordAction;

View File

@ -1,5 +1,5 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { Select, SelectOption } from '@/ui/input/components/Select'; import { Select } from '@/ui/input/components/Select';
import { WorkflowFindRecordsAction } from '@/workflow/types/Workflow'; import { WorkflowFindRecordsAction } from '@/workflow/types/Workflow';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader'; import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@ -9,9 +9,9 @@ import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowS
import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow'; import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow';
import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow'; import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { HorizontalSeparator, useIcons } from 'twenty-ui';
import { useDebouncedCallback } from 'use-debounce';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { HorizontalSeparator, SelectOption, useIcons } from 'twenty-ui';
import { useDebouncedCallback } from 'use-debounce';
type WorkflowEditActionFindRecordsProps = { type WorkflowEditActionFindRecordsProps = {
action: WorkflowFindRecordsAction; action: WorkflowFindRecordsAction;

View File

@ -7,7 +7,7 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput'; import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth'; import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { Select, SelectOption } from '@/ui/input/components/Select'; import { Select } from '@/ui/input/components/Select';
import { workflowIdState } from '@/workflow/states/workflowIdState'; import { workflowIdState } from '@/workflow/states/workflowIdState';
import { WorkflowSendEmailAction } from '@/workflow/types/Workflow'; import { WorkflowSendEmailAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
@ -18,12 +18,12 @@ import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { IconPlus, useIcons } from 'twenty-ui'; import { ConnectedAccountProvider } from 'twenty-shared/types';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { IconPlus, SelectOption, useIcons } from 'twenty-ui';
import { JsonValue } from 'type-fest'; import { JsonValue } from 'type-fest';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { ConnectedAccountProvider } from 'twenty-shared/types';
type WorkflowEditActionSendEmailProps = { type WorkflowEditActionSendEmailProps = {
action: WorkflowSendEmailAction; action: WorkflowSendEmailAction;

View File

@ -1,5 +1,5 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { Select, SelectOption } from '@/ui/input/components/Select'; import { Select } from '@/ui/input/components/Select';
import { WorkflowUpdateRecordAction } from '@/workflow/types/Workflow'; import { WorkflowUpdateRecordAction } from '@/workflow/types/Workflow';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@ -13,11 +13,11 @@ import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-a
import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow'; import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { HorizontalSeparator, useIcons } from 'twenty-ui'; import { isDefined } from 'twenty-shared/utils';
import { HorizontalSeparator, SelectOption, useIcons } from 'twenty-ui';
import { JsonValue } from 'type-fest'; import { JsonValue } from 'type-fest';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isDefined } from 'twenty-shared/utils';
type WorkflowEditActionUpdateRecordProps = { type WorkflowEditActionUpdateRecordProps = {
action: WorkflowUpdateRecordAction; action: WorkflowUpdateRecordAction;

View File

@ -8,6 +8,7 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import camelCase from 'lodash.camelcase'; import camelCase from 'lodash.camelcase';
import { FieldMetadataType } from 'twenty-shared/types';
import { import {
IconSettingsAutomation, IconSettingsAutomation,
IconX, IconX,
@ -15,7 +16,6 @@ import {
IllustrationIconText, IllustrationIconText,
LightIconButton, LightIconButton,
} from 'twenty-ui'; } from 'twenty-ui';
import { FieldMetadataType } from 'twenty-shared/types';
type WorkflowEditActionFormFieldSettingsProps = { type WorkflowEditActionFormFieldSettingsProps = {
field: WorkflowFormActionField; field: WorkflowFormActionField;
@ -109,13 +109,13 @@ export const WorkflowEditActionFormFieldSettings = ({
label: getDefaultFormFieldSettings(FieldMetadataType.TEXT) label: getDefaultFormFieldSettings(FieldMetadataType.TEXT)
.label, .label,
value: FieldMetadataType.TEXT, value: FieldMetadataType.TEXT,
icon: IllustrationIconText, Icon: IllustrationIconText,
}, },
{ {
label: getDefaultFormFieldSettings(FieldMetadataType.NUMBER) label: getDefaultFormFieldSettings(FieldMetadataType.NUMBER)
.label, .label,
value: FieldMetadataType.NUMBER, value: FieldMetadataType.NUMBER,
icon: IllustrationIconNumbers, Icon: IllustrationIconNumbers,
}, },
]} ]}
onChange={(newType: string | null) => { onChange={(newType: string | null) => {

View File

@ -1,5 +1,5 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { Select, SelectOption } from '@/ui/input/components/Select'; import { Select } from '@/ui/input/components/Select';
import { WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow'; import { WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow';
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName'; import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
@ -7,8 +7,8 @@ import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/Workflo
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon'; import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel'; import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { useIcons } from 'twenty-ui';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { SelectOption, useIcons } from 'twenty-ui';
type WorkflowEditTriggerDatabaseEventFormProps = { type WorkflowEditTriggerDatabaseEventFormProps = {
trigger: WorkflowDatabaseEventTrigger; trigger: WorkflowDatabaseEventTrigger;

View File

@ -1,5 +1,5 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { Select, SelectOption } from '@/ui/input/components/Select'; import { Select } from '@/ui/input/components/Select';
import { import {
WorkflowManualTrigger, WorkflowManualTrigger,
WorkflowManualTriggerAvailability, WorkflowManualTriggerAvailability,
@ -10,8 +10,8 @@ import { MANUAL_TRIGGER_AVAILABILITY_OPTIONS } from '@/workflow/workflow-trigger
import { getManualTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettings'; import { getManualTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettings';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon'; import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { useIcons } from 'twenty-ui';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { SelectOption, useIcons } from 'twenty-ui';
type WorkflowEditTriggerManualFormProps = { type WorkflowEditTriggerManualFormProps = {
trigger: WorkflowManualTrigger; trigger: WorkflowManualTrigger;

View File

@ -12,6 +12,7 @@ import {
IconRefresh, IconRefresh,
IconTrash, IconTrash,
Section, Section,
SelectOption,
useIcons, useIcons,
} from 'twenty-ui'; } from 'twenty-ui';
@ -19,14 +20,14 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { useWebhookUpdateForm } from '@/settings/developers/hooks/useWebhookUpdateForm'; import { useWebhookUpdateForm } from '@/settings/developers/hooks/useWebhookUpdateForm';
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { Select, SelectOption } from '@/ui/input/components/Select'; import { Select } from '@/ui/input/components/Select';
import { TextArea } from '@/ui/input/components/TextArea'; import { TextArea } from '@/ui/input/components/TextArea';
import { TextInput } from '@/ui/input/components/TextInput'; import { TextInput } from '@/ui/input/components/TextInput';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { Trans, useLingui } from '@lingui/react/macro'; import { Trans, useLingui } from '@lingui/react/macro';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const OBJECT_DROPDOWN_WIDTH = 340; const OBJECT_DROPDOWN_WIDTH = 340;
const ACTION_DROPDOWN_WIDTH = 140; const ACTION_DROPDOWN_WIDTH = 140;

View File

@ -31,6 +31,7 @@ const StyledTag = styled.h3<{
const themeColor = theme.tag.background[color]; const themeColor = theme.tag.background[color];
if (!isDefined(themeColor)) { if (!isDefined(themeColor)) {
// eslint-disable-next-line no-console
console.warn(`Tag color ${color} is not defined in the theme`); console.warn(`Tag color ${color} is not defined in the theme`);
return theme.tag.background.gray; return theme.tag.background.gray;
} else { } else {

View File

@ -27,3 +27,4 @@ export * from './components/Radio';
export * from './components/RadioGroup'; export * from './components/RadioGroup';
export * from './components/Toggle'; export * from './components/Toggle';
export * from './types/ColorScheme'; export * from './types/ColorScheme';
export * from './types/SelectOption';

View File

@ -0,0 +1,12 @@
import { IconComponent } from '@ui/display';
import { ThemeColor } from '@ui/theme';
export type SelectOption<
Value extends string | number | boolean | null = string,
> = {
Icon?: IconComponent | null;
label: string;
value: Value;
disabled?: boolean;
color?: ThemeColor | 'transparent';
};