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:
@ -1,9 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
import { IconCircleOff, IconComponentProps } from 'twenty-ui';
|
||||
import { IconCircleOff, IconComponentProps, SelectOption } from 'twenty-ui';
|
||||
|
||||
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
|
||||
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||
import { SelectOption } from '@/spreadsheet-import/types';
|
||||
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
|
||||
import { CountryCode } from 'libphonenumber-js';
|
||||
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
import { IconCircleOff, IconComponentProps } from 'twenty-ui';
|
||||
import { IconCircleOff, IconComponentProps, SelectOption } from 'twenty-ui';
|
||||
|
||||
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
|
||||
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||
import { SelectOption } from '@/spreadsheet-import/types';
|
||||
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
|
||||
|
||||
export const FormCountrySelectInput = ({
|
||||
|
||||
@ -8,7 +8,6 @@ import { FormMultiSelectFieldInputHotKeyScope } from '@/object-record/record-fie
|
||||
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||
import { SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-field/meta-types/input/constants/SelectFieldInputSelectableListComponentInstanceId';
|
||||
import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { SelectOption } from '@/spreadsheet-import/types';
|
||||
import { MultiSelectDisplay } from '@/ui/field/display/components/MultiSelectDisplay';
|
||||
import { MultiSelectInput } from '@/ui/field/input/components/MultiSelectInput';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
@ -17,8 +16,8 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH
|
||||
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useId, useState } from 'react';
|
||||
import { IconChevronDown, VisibilityHidden } from 'twenty-ui';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconChevronDown, SelectOption, VisibilityHidden } from 'twenty-ui';
|
||||
|
||||
type FormMultiSelectFieldInputProps = {
|
||||
label?: string;
|
||||
|
||||
@ -5,7 +5,6 @@ import { VariableChipStandalone } from '@/object-record/record-field/form-types/
|
||||
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||
import { SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-field/meta-types/input/constants/SelectFieldInputSelectableListComponentInstanceId';
|
||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||
import { SelectOption } from '@/spreadsheet-import/types';
|
||||
import { SelectDisplay } from '@/ui/field/display/components/SelectDisplay';
|
||||
import { SelectInput } from '@/ui/field/input/components/SelectInput';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
@ -18,8 +17,8 @@ import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useId, useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { IconChevronDown, VisibilityHidden } from 'twenty-ui';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconChevronDown, SelectOption, VisibilityHidden } from 'twenty-ui';
|
||||
|
||||
type FormSelectFieldInputProps = {
|
||||
label?: string;
|
||||
@ -245,7 +244,7 @@ export const FormSelectFieldInput = ({
|
||||
<SelectDisplay
|
||||
color={selectedOption.color ?? 'transparent'}
|
||||
label={selectedOption.label}
|
||||
Icon={selectedOption.icon ?? undefined}
|
||||
Icon={selectedOption.Icon ?? undefined}
|
||||
preventPadding={preventDisplayPadding}
|
||||
/>
|
||||
</StyledSelectDisplayContainer>
|
||||
@ -269,7 +268,7 @@ export const FormSelectFieldInput = ({
|
||||
<SelectDisplay
|
||||
color={selectedOption.color ?? 'transparent'}
|
||||
label={selectedOption.label}
|
||||
Icon={selectedOption.icon ?? undefined}
|
||||
Icon={selectedOption.Icon ?? undefined}
|
||||
preventPadding={preventDisplayPadding}
|
||||
/>
|
||||
</StyledSelectDisplayContainer>
|
||||
|
||||
@ -2,14 +2,13 @@ import { useClearField } from '@/object-record/record-field/hooks/useClearField'
|
||||
import { useSelectField } from '@/object-record/record-field/meta-types/hooks/useSelectField';
|
||||
import { SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-field/meta-types/input/constants/SelectFieldInputSelectableListComponentInstanceId';
|
||||
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { SelectOption } from '@/spreadsheet-import/types';
|
||||
import { SelectInput } from '@/ui/field/input/components/SelectInput';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { SelectOption } from 'twenty-ui';
|
||||
type SelectFieldInputProps = {
|
||||
onSubmit?: FieldInputEvent;
|
||||
onCancel?: () => void;
|
||||
|
||||
@ -7,6 +7,11 @@ import { AvailableFieldForImport } from '@/object-record/spreadsheet-import/type
|
||||
import { getSpreadSheetFieldValidationDefinitions } from '@/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
type CompositeFieldType = keyof typeof COMPOSITE_FIELD_IMPORT_LABELS;
|
||||
|
||||
// Helper type for field validation type resolvers
|
||||
type ValidationTypeResolver = (key: string, label: string) => FieldMetadataType;
|
||||
|
||||
export const useBuildAvailableFieldsForImport = () => {
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
@ -15,243 +20,160 @@ export const useBuildAvailableFieldsForImport = () => {
|
||||
) => {
|
||||
const availableFieldsForImport: AvailableFieldForImport[] = [];
|
||||
|
||||
// Todo: refactor this to avoid this else if syntax with duplicated code
|
||||
const createBaseField = (
|
||||
fieldMetadataItem: FieldMetadataItem,
|
||||
overrides: Partial<AvailableFieldForImport> = {},
|
||||
customLabel?: string,
|
||||
): AvailableFieldForImport => ({
|
||||
Icon: getIcon(fieldMetadataItem.icon),
|
||||
label: customLabel ?? fieldMetadataItem.label,
|
||||
key: fieldMetadataItem.name,
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: fieldMetadataItem.type,
|
||||
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
|
||||
fieldMetadataItem.type,
|
||||
customLabel ?? fieldMetadataItem.label,
|
||||
),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const handleCompositeFieldWithLabels = (
|
||||
fieldMetadataItem: FieldMetadataItem,
|
||||
fieldType: CompositeFieldType,
|
||||
validationTypeResolver?: ValidationTypeResolver,
|
||||
) => {
|
||||
Object.entries(COMPOSITE_FIELD_IMPORT_LABELS[fieldType]).forEach(
|
||||
([key, fieldLabel]) => {
|
||||
const label = `${fieldLabel} (${fieldMetadataItem.label})`;
|
||||
// Use the custom validation type if provided, otherwise use the field's type
|
||||
const validationType = validationTypeResolver
|
||||
? validationTypeResolver(key, fieldLabel)
|
||||
: fieldMetadataItem.type;
|
||||
|
||||
availableFieldsForImport.push(
|
||||
createBaseField(fieldMetadataItem, {
|
||||
label,
|
||||
key: `${fieldLabel} (${fieldMetadataItem.name})`,
|
||||
fieldValidationDefinitions:
|
||||
getSpreadSheetFieldValidationDefinitions(validationType, label),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleSelectField = (
|
||||
fieldMetadataItem: FieldMetadataItem,
|
||||
isMulti = false,
|
||||
) => {
|
||||
availableFieldsForImport.push(
|
||||
createBaseField(fieldMetadataItem, {
|
||||
fieldType: {
|
||||
type: isMulti ? 'multiSelect' : 'select',
|
||||
options:
|
||||
fieldMetadataItem.options?.map((option) => ({
|
||||
label: option.label,
|
||||
value: option.value,
|
||||
color: option.color,
|
||||
})) || [],
|
||||
},
|
||||
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
|
||||
fieldMetadataItem.type,
|
||||
`${fieldMetadataItem.label} (ID)`,
|
||||
),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// Special validation type resolver for currency fields
|
||||
const currencyValidationResolver: ValidationTypeResolver = (key) =>
|
||||
key === 'amountMicrosLabel'
|
||||
? FieldMetadataType.NUMBER
|
||||
: FieldMetadataType.CURRENCY;
|
||||
|
||||
const fieldTypeHandlers: Record<
|
||||
string,
|
||||
(fieldMetadataItem: FieldMetadataItem) => void
|
||||
> = {
|
||||
[FieldMetadataType.FULL_NAME]: (fieldMetadataItem) => {
|
||||
handleCompositeFieldWithLabels(
|
||||
fieldMetadataItem,
|
||||
FieldMetadataType.FULL_NAME,
|
||||
);
|
||||
},
|
||||
[FieldMetadataType.ADDRESS]: (fieldMetadataItem) => {
|
||||
handleCompositeFieldWithLabels(
|
||||
fieldMetadataItem,
|
||||
FieldMetadataType.ADDRESS,
|
||||
);
|
||||
},
|
||||
[FieldMetadataType.LINKS]: (fieldMetadataItem) => {
|
||||
handleCompositeFieldWithLabels(
|
||||
fieldMetadataItem,
|
||||
FieldMetadataType.LINKS,
|
||||
);
|
||||
},
|
||||
[FieldMetadataType.EMAILS]: (fieldMetadataItem) => {
|
||||
handleCompositeFieldWithLabels(
|
||||
fieldMetadataItem,
|
||||
FieldMetadataType.EMAILS,
|
||||
);
|
||||
},
|
||||
[FieldMetadataType.PHONES]: (fieldMetadataItem) => {
|
||||
handleCompositeFieldWithLabels(
|
||||
fieldMetadataItem,
|
||||
FieldMetadataType.PHONES,
|
||||
);
|
||||
},
|
||||
[FieldMetadataType.RICH_TEXT_V2]: (fieldMetadataItem) => {
|
||||
handleCompositeFieldWithLabels(
|
||||
fieldMetadataItem,
|
||||
FieldMetadataType.RICH_TEXT_V2,
|
||||
);
|
||||
},
|
||||
[FieldMetadataType.CURRENCY]: (fieldMetadataItem) => {
|
||||
handleCompositeFieldWithLabels(
|
||||
fieldMetadataItem,
|
||||
FieldMetadataType.CURRENCY,
|
||||
currencyValidationResolver,
|
||||
);
|
||||
},
|
||||
[FieldMetadataType.RELATION]: (fieldMetadataItem) => {
|
||||
const label = `${fieldMetadataItem.label} (ID)`;
|
||||
availableFieldsForImport.push(
|
||||
createBaseField(fieldMetadataItem, {
|
||||
label,
|
||||
fieldValidationDefinitions:
|
||||
getSpreadSheetFieldValidationDefinitions(
|
||||
fieldMetadataItem.type,
|
||||
label,
|
||||
),
|
||||
}),
|
||||
);
|
||||
},
|
||||
[FieldMetadataType.SELECT]: (fieldMetadataItem) => {
|
||||
handleSelectField(fieldMetadataItem, false);
|
||||
},
|
||||
[FieldMetadataType.MULTI_SELECT]: (fieldMetadataItem) => {
|
||||
handleSelectField(fieldMetadataItem, true);
|
||||
},
|
||||
[FieldMetadataType.BOOLEAN]: (fieldMetadataItem) => {
|
||||
availableFieldsForImport.push(
|
||||
createBaseField(fieldMetadataItem, {
|
||||
fieldType: { type: 'checkbox' },
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
default: (fieldMetadataItem) => {
|
||||
availableFieldsForImport.push(createBaseField(fieldMetadataItem));
|
||||
},
|
||||
};
|
||||
|
||||
for (const fieldMetadataItem of fieldMetadataItems) {
|
||||
if (fieldMetadataItem.type === FieldMetadataType.FULL_NAME) {
|
||||
const { firstNameLabel, lastNameLabel } =
|
||||
COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.FULL_NAME];
|
||||
|
||||
availableFieldsForImport.push({
|
||||
icon: getIcon(fieldMetadataItem.icon),
|
||||
label: `${firstNameLabel} (${fieldMetadataItem.label})`,
|
||||
key: `${firstNameLabel} (${fieldMetadataItem.name})`,
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
fieldMetadataType: fieldMetadataItem.type,
|
||||
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
|
||||
fieldMetadataItem.type,
|
||||
`${firstNameLabel} (${fieldMetadataItem.label})`,
|
||||
),
|
||||
});
|
||||
|
||||
availableFieldsForImport.push({
|
||||
icon: getIcon(fieldMetadataItem.icon),
|
||||
label: `${lastNameLabel} (${fieldMetadataItem.label})`,
|
||||
key: `${lastNameLabel} (${fieldMetadataItem.name})`,
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
fieldMetadataType: fieldMetadataItem.type,
|
||||
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
|
||||
fieldMetadataItem.type,
|
||||
`${lastNameLabel} (${fieldMetadataItem.label})`,
|
||||
),
|
||||
});
|
||||
} else if (fieldMetadataItem.type === FieldMetadataType.RELATION) {
|
||||
availableFieldsForImport.push({
|
||||
icon: getIcon(fieldMetadataItem.icon),
|
||||
label: fieldMetadataItem.label + ' (ID)',
|
||||
key: fieldMetadataItem.name,
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
fieldMetadataType: fieldMetadataItem.type,
|
||||
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
|
||||
fieldMetadataItem.type,
|
||||
fieldMetadataItem.label + ' (ID)',
|
||||
),
|
||||
});
|
||||
} else if (fieldMetadataItem.type === FieldMetadataType.CURRENCY) {
|
||||
const { currencyCodeLabel, amountMicrosLabel } =
|
||||
COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.CURRENCY];
|
||||
|
||||
availableFieldsForImport.push({
|
||||
icon: getIcon(fieldMetadataItem.icon),
|
||||
label: `${currencyCodeLabel} (${fieldMetadataItem.label})`,
|
||||
key: `${currencyCodeLabel} (${fieldMetadataItem.name})`,
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
fieldMetadataType: fieldMetadataItem.type,
|
||||
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
|
||||
fieldMetadataItem.type,
|
||||
`${currencyCodeLabel} (${fieldMetadataItem.label})`,
|
||||
),
|
||||
});
|
||||
|
||||
availableFieldsForImport.push({
|
||||
icon: getIcon(fieldMetadataItem.icon),
|
||||
label: `${amountMicrosLabel} (${fieldMetadataItem.label})`,
|
||||
key: `${amountMicrosLabel} (${fieldMetadataItem.name})`,
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
fieldMetadataType: fieldMetadataItem.type,
|
||||
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
|
||||
FieldMetadataType.NUMBER,
|
||||
`${amountMicrosLabel} (${fieldMetadataItem.label})`,
|
||||
),
|
||||
});
|
||||
} else if (fieldMetadataItem.type === FieldMetadataType.ADDRESS) {
|
||||
Object.entries(
|
||||
COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.ADDRESS],
|
||||
).forEach(([_, fieldLabel]) => {
|
||||
availableFieldsForImport.push({
|
||||
icon: getIcon(fieldMetadataItem.icon),
|
||||
label: `${fieldLabel} (${fieldMetadataItem.label})`,
|
||||
key: `${fieldLabel} (${fieldMetadataItem.name})`,
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
fieldMetadataType: fieldMetadataItem.type,
|
||||
fieldValidationDefinitions:
|
||||
getSpreadSheetFieldValidationDefinitions(
|
||||
fieldMetadataItem.type,
|
||||
`${fieldLabel} (${fieldMetadataItem.label})`,
|
||||
),
|
||||
});
|
||||
});
|
||||
} else if (fieldMetadataItem.type === FieldMetadataType.LINKS) {
|
||||
Object.entries(
|
||||
COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.LINKS],
|
||||
).forEach(([_, fieldLabel]) => {
|
||||
availableFieldsForImport.push({
|
||||
icon: getIcon(fieldMetadataItem.icon),
|
||||
label: `${fieldLabel} (${fieldMetadataItem.label})`,
|
||||
key: `${fieldLabel} (${fieldMetadataItem.name})`,
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
fieldMetadataType: fieldMetadataItem.type,
|
||||
fieldValidationDefinitions:
|
||||
getSpreadSheetFieldValidationDefinitions(
|
||||
fieldMetadataItem.type,
|
||||
`${fieldLabel} (${fieldMetadataItem.label})`,
|
||||
),
|
||||
});
|
||||
});
|
||||
} else if (fieldMetadataItem.type === FieldMetadataType.SELECT) {
|
||||
availableFieldsForImport.push({
|
||||
icon: getIcon(fieldMetadataItem.icon),
|
||||
label: fieldMetadataItem.label,
|
||||
key: fieldMetadataItem.name,
|
||||
fieldType: {
|
||||
type: 'select',
|
||||
options:
|
||||
fieldMetadataItem.options?.map((option) => ({
|
||||
label: option.label,
|
||||
value: option.value,
|
||||
color: option.color,
|
||||
})) || [],
|
||||
},
|
||||
fieldMetadataType: fieldMetadataItem.type,
|
||||
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
|
||||
fieldMetadataItem.type,
|
||||
fieldMetadataItem.label + ' (ID)',
|
||||
),
|
||||
});
|
||||
} else if (fieldMetadataItem.type === FieldMetadataType.MULTI_SELECT) {
|
||||
availableFieldsForImport.push({
|
||||
icon: getIcon(fieldMetadataItem.icon),
|
||||
label: fieldMetadataItem.label,
|
||||
key: fieldMetadataItem.name,
|
||||
fieldType: {
|
||||
type: 'multiSelect',
|
||||
options:
|
||||
fieldMetadataItem.options?.map((option) => ({
|
||||
label: option.label,
|
||||
value: option.value,
|
||||
color: option.color,
|
||||
})) || [],
|
||||
},
|
||||
fieldMetadataType: fieldMetadataItem.type,
|
||||
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
|
||||
fieldMetadataItem.type,
|
||||
fieldMetadataItem.label + ' (ID)',
|
||||
),
|
||||
});
|
||||
} else if (fieldMetadataItem.type === FieldMetadataType.BOOLEAN) {
|
||||
availableFieldsForImport.push({
|
||||
icon: getIcon(fieldMetadataItem.icon),
|
||||
label: fieldMetadataItem.label,
|
||||
key: fieldMetadataItem.name,
|
||||
fieldType: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
fieldMetadataType: fieldMetadataItem.type,
|
||||
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
|
||||
fieldMetadataItem.type,
|
||||
fieldMetadataItem.label,
|
||||
),
|
||||
});
|
||||
} else if (fieldMetadataItem.type === FieldMetadataType.EMAILS) {
|
||||
Object.entries(
|
||||
COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.EMAILS],
|
||||
).forEach(([_, fieldLabel]) => {
|
||||
availableFieldsForImport.push({
|
||||
icon: getIcon(fieldMetadataItem.icon),
|
||||
label: `${fieldLabel} (${fieldMetadataItem.label})`,
|
||||
key: `${fieldLabel} (${fieldMetadataItem.name})`,
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
fieldMetadataType: fieldMetadataItem.type,
|
||||
fieldValidationDefinitions:
|
||||
getSpreadSheetFieldValidationDefinitions(
|
||||
fieldMetadataItem.type,
|
||||
`${fieldLabel} (${fieldMetadataItem.label})`,
|
||||
),
|
||||
});
|
||||
});
|
||||
} else if (fieldMetadataItem.type === FieldMetadataType.PHONES) {
|
||||
Object.entries(
|
||||
COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.PHONES],
|
||||
).forEach(([_, fieldLabel]) => {
|
||||
availableFieldsForImport.push({
|
||||
icon: getIcon(fieldMetadataItem.icon),
|
||||
label: `${fieldLabel} (${fieldMetadataItem.label})`,
|
||||
key: `${fieldLabel} (${fieldMetadataItem.name})`,
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
fieldMetadataType: fieldMetadataItem.type,
|
||||
fieldValidationDefinitions:
|
||||
getSpreadSheetFieldValidationDefinitions(
|
||||
fieldMetadataItem.type,
|
||||
`${fieldLabel} (${fieldMetadataItem.label})`,
|
||||
),
|
||||
});
|
||||
});
|
||||
} else if (fieldMetadataItem.type === FieldMetadataType.RICH_TEXT_V2) {
|
||||
Object.entries(
|
||||
COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.RICH_TEXT_V2],
|
||||
).forEach(([_, fieldLabel]) => {
|
||||
availableFieldsForImport.push({
|
||||
icon: getIcon(fieldMetadataItem.icon),
|
||||
label: `${fieldLabel} (${fieldMetadataItem.label})`,
|
||||
key: `${fieldLabel} (${fieldMetadataItem.name})`,
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
fieldMetadataType: fieldMetadataItem.type,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
availableFieldsForImport.push({
|
||||
icon: getIcon(fieldMetadataItem.icon),
|
||||
label: fieldMetadataItem.label,
|
||||
key: fieldMetadataItem.name,
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
fieldMetadataType: fieldMetadataItem.type,
|
||||
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
|
||||
fieldMetadataItem.type,
|
||||
fieldMetadataItem.label,
|
||||
),
|
||||
});
|
||||
}
|
||||
const handler =
|
||||
fieldTypeHandlers[fieldMetadataItem.type] || fieldTypeHandlers.default;
|
||||
handler(fieldMetadataItem);
|
||||
}
|
||||
|
||||
return availableFieldsForImport;
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import {
|
||||
FieldValidationDefinition,
|
||||
SpreadsheetImportFieldType,
|
||||
SpreadsheetImportFieldValidationDefinition,
|
||||
} from '@/spreadsheet-import/types';
|
||||
import { IconComponent } from 'twenty-ui';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
export type AvailableFieldForImport = {
|
||||
icon: IconComponent;
|
||||
Icon: IconComponent;
|
||||
label: string;
|
||||
key: string;
|
||||
fieldType: SpreadsheetImportFieldType;
|
||||
fieldValidationDefinitions?: FieldValidationDefinition[];
|
||||
fieldValidationDefinitions?: SpreadsheetImportFieldValidationDefinition[];
|
||||
fieldMetadataType: FieldMetadataType;
|
||||
};
|
||||
|
||||
@ -10,11 +10,11 @@ import {
|
||||
import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels';
|
||||
import { ImportedStructuredRow } from '@/spreadsheet-import/types';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { z } from 'zod';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { castToString } from '~/utils/castToString';
|
||||
import { convertCurrencyAmountToCurrencyMicros } from '~/utils/convertCurrencyToCurrencyMicros';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type BuildRecordFromImportedStructuredRowArgs = {
|
||||
importedStructuredRow: ImportedStructuredRow<any>;
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { FieldValidationDefinition } from '@/spreadsheet-import/types';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { SpreadsheetImportFieldValidationDefinition } from '@/spreadsheet-import/types';
|
||||
import { absoluteUrlSchema, isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const getSpreadSheetFieldValidationDefinitions = (
|
||||
type: FieldMetadataType,
|
||||
fieldName: string,
|
||||
): FieldValidationDefinition[] => {
|
||||
): SpreadsheetImportFieldValidationDefinition[] => {
|
||||
switch (type) {
|
||||
case FieldMetadataType.FULL_NAME:
|
||||
return [
|
||||
|
||||
@ -2,10 +2,10 @@
|
||||
/* eslint-disable @nx/workspace-max-consts-per-file */
|
||||
import { IANA_TIME_ZONES } from '@/localization/constants/IanaTimeZones';
|
||||
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<
|
||||
Record<string, SelectOption<string>>
|
||||
Record<string, SelectOption>
|
||||
>((result, ianaTimeZone) => {
|
||||
const timeZoneLabel = formatTimeZoneLabel(ianaTimeZone);
|
||||
|
||||
|
||||
@ -4,12 +4,17 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { addressSchema as addressFieldDefaultValueSchema } from '@/object-record/record-field/types/guards/isFieldAddressValue';
|
||||
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
|
||||
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
|
||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||
import { IconCircleOff, IconComponentProps, IconMap } from 'twenty-ui';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import {
|
||||
IconCircleOff,
|
||||
IconComponentProps,
|
||||
IconMap,
|
||||
SelectOption,
|
||||
} from 'twenty-ui';
|
||||
import { z } from 'zod';
|
||||
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
|
||||
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
type SettingsDataModelFieldAddressFormProps = {
|
||||
disabled?: boolean;
|
||||
defaultCountry?: string;
|
||||
@ -41,7 +46,7 @@ export const SettingsDataModelFieldAddressForm = ({
|
||||
},
|
||||
...useCountries()
|
||||
.sort((a, b) => a.countryName.localeCompare(b.countryName))
|
||||
.map<SelectOption<string>>(({ countryName, Flag }) => ({
|
||||
.map<SelectOption>(({ countryName, Flag }) => ({
|
||||
label: countryName,
|
||||
value: countryName,
|
||||
Icon: (props: IconComponentProps) =>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useMemo } from 'react';
|
||||
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 { 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 { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
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 { t } from '@lingui/core/macro';
|
||||
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { defaultSpreadsheetImportProps } from '@/spreadsheet-import/provider/components/SpreadsheetImport';
|
||||
import { Columns } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import {
|
||||
Fields,
|
||||
SpreadsheetImportDialogOptions,
|
||||
SpreadsheetImportFields
|
||||
} from '@/spreadsheet-import/types';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
const fields = [
|
||||
{
|
||||
icon: null,
|
||||
Icon: null,
|
||||
label: 'Name',
|
||||
key: 'name',
|
||||
alternateMatches: ['first name', 'first'],
|
||||
@ -26,7 +26,7 @@ const fields = [
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
},
|
||||
{
|
||||
icon: null,
|
||||
Icon: null,
|
||||
label: 'Surname',
|
||||
key: 'surname',
|
||||
alternateMatches: ['second name', 'last name', 'last'],
|
||||
@ -44,7 +44,7 @@ const fields = [
|
||||
description: 'Family / Last name',
|
||||
},
|
||||
{
|
||||
icon: null,
|
||||
Icon: null,
|
||||
label: 'Age',
|
||||
key: 'age',
|
||||
alternateMatches: ['years'],
|
||||
@ -62,7 +62,7 @@ const fields = [
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: null,
|
||||
Icon: null,
|
||||
label: 'Team',
|
||||
key: 'team',
|
||||
alternateMatches: ['department'],
|
||||
@ -82,7 +82,7 @@ const fields = [
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: null,
|
||||
Icon: null,
|
||||
label: 'Is manager',
|
||||
key: 'is_manager',
|
||||
alternateMatches: ['manages'],
|
||||
@ -92,9 +92,9 @@ const fields = [
|
||||
},
|
||||
example: 'true',
|
||||
},
|
||||
] as Fields<string>;
|
||||
] as SpreadsheetImportFields<string>;
|
||||
|
||||
export const importedColums: Columns<string> = [
|
||||
export const importedColums: SpreadsheetColumns<string> = [
|
||||
{
|
||||
header: 'Name',
|
||||
index: 0,
|
||||
|
||||
@ -9,11 +9,10 @@ import {
|
||||
} from '@floating-ui/react';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
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 { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { SelectOption } from '@/spreadsheet-import/types';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
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 { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
import { useUpdateEffect } from '~/hooks/useUpdateEffect';
|
||||
|
||||
const StyledFloatingDropdown = styled.div`
|
||||
@ -116,7 +116,7 @@ export const MatchColumnSelect = ({
|
||||
<>
|
||||
<div ref={refs.setReference}>
|
||||
<MenuItem
|
||||
LeftIcon={value?.icon}
|
||||
LeftIcon={value?.Icon}
|
||||
onClick={handleDropdownItemClick}
|
||||
text={value?.label ?? placeholder ?? ''}
|
||||
accent={value?.label ? 'default' : 'placeholder'}
|
||||
@ -138,31 +138,36 @@ export const MatchColumnSelect = ({
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{options?.map((option) => (
|
||||
<React.Fragment key={option.label}>
|
||||
<MenuItemSelect
|
||||
selected={value?.label === option.label}
|
||||
onClick={() => handleChange(option)}
|
||||
disabled={
|
||||
option.disabled && value?.value !== option.value
|
||||
}
|
||||
LeftIcon={option?.icon}
|
||||
text={option.label}
|
||||
/>
|
||||
{option.disabled &&
|
||||
value?.value !== option.value &&
|
||||
createPortal(
|
||||
<AppTooltip
|
||||
key={option.value}
|
||||
anchorSelect={`#${option.value}`}
|
||||
content={t`You are already importing this column.`}
|
||||
place="right"
|
||||
offset={-20}
|
||||
/>,
|
||||
document.body,
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
{options?.map((option) => {
|
||||
const id = `${uuidV4()}-${option.value}`;
|
||||
return (
|
||||
<React.Fragment key={id}>
|
||||
<div id={id}>
|
||||
<MenuItemSelect
|
||||
selected={value?.label === option.label}
|
||||
onClick={() => handleChange(option)}
|
||||
disabled={
|
||||
option.disabled && value?.value !== option.value
|
||||
}
|
||||
LeftIcon={option?.Icon}
|
||||
text={option.label}
|
||||
/>
|
||||
</div>
|
||||
{option.disabled &&
|
||||
value?.value !== option.value &&
|
||||
createPortal(
|
||||
<AppTooltip
|
||||
key={id}
|
||||
anchorSelect={`#${id}`}
|
||||
content={t`You are already importing this column.`}
|
||||
place="right"
|
||||
offset={-20}
|
||||
/>,
|
||||
document.body,
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{options?.length === 0 && (
|
||||
<MenuItem key="No results" text={t`No results`} />
|
||||
)}
|
||||
|
||||
@ -5,9 +5,9 @@ import { Heading } from '@/spreadsheet-import/components/Heading';
|
||||
import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton';
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import {
|
||||
Field,
|
||||
ImportedRow,
|
||||
ImportedStructuredRow,
|
||||
SpreadsheetImportField,
|
||||
} from '@/spreadsheet-import/types';
|
||||
import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields';
|
||||
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 { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep';
|
||||
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 { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilState } from 'recoil';
|
||||
@ -68,68 +71,6 @@ export type MatchColumnsStepProps = {
|
||||
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>({
|
||||
data,
|
||||
headerValues,
|
||||
@ -179,7 +120,7 @@ export const MatchColumnsStep = <T extends string>({
|
||||
const onChange = useCallback(
|
||||
(value: T, columnIndex: number) => {
|
||||
if (value === 'do-not-import') {
|
||||
if (columns[columnIndex].type === ColumnType.ignored) {
|
||||
if (columns[columnIndex].type === SpreadsheetColumnType.ignored) {
|
||||
onRevertIgnore(columnIndex);
|
||||
} else {
|
||||
onIgnore(columnIndex);
|
||||
@ -187,12 +128,12 @@ export const MatchColumnsStep = <T extends string>({
|
||||
} else {
|
||||
const field = fields.find(
|
||||
(field) => field.key === value,
|
||||
) as unknown as Field<T>;
|
||||
) as unknown as SpreadsheetImportField<T>;
|
||||
const existingFieldIndex = columns.findIndex(
|
||||
(column) => 'value' in column && column.value === field.key,
|
||||
);
|
||||
setColumns(
|
||||
columns.map<Column<string>>((column, index) => {
|
||||
columns.map<SpreadsheetColumn<string>>((column, index) => {
|
||||
if (columnIndex === index) {
|
||||
return setColumn(column, field, data);
|
||||
} else if (index === existingFieldIndex) {
|
||||
@ -223,7 +164,7 @@ export const MatchColumnsStep = <T extends string>({
|
||||
async (
|
||||
values: ImportedStructuredRow<string>[],
|
||||
rawData: ImportedRow[],
|
||||
columns: Columns<string>,
|
||||
columns: SpreadsheetColumns<string>,
|
||||
) => {
|
||||
try {
|
||||
const data = await matchColumnsStepHook(values, rawData, columns);
|
||||
@ -322,7 +263,7 @@ export const MatchColumnsStep = <T extends string>({
|
||||
|
||||
useEffect(() => {
|
||||
const isInitialColumnsState = columns.every(
|
||||
(column) => column.type === ColumnType.empty,
|
||||
(column) => column.type === SpreadsheetColumnType.empty,
|
||||
);
|
||||
if (autoMapHeaders && isInitialColumnsState) {
|
||||
setColumns(getMatchedColumns(columns, fields, data, autoMapDistance));
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||
import styled from '@emotion/styled';
|
||||
import React from 'react';
|
||||
|
||||
import { Columns } from '../MatchColumnsStep';
|
||||
|
||||
const StyledGridContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@ -93,17 +92,17 @@ const StyledGridHeader = styled.div<PositionProps>`
|
||||
`;
|
||||
|
||||
type ColumnGridProps<T extends string> = {
|
||||
columns: Columns<T>;
|
||||
columns: SpreadsheetColumns<T>;
|
||||
renderUserColumn: (
|
||||
columns: Columns<T>,
|
||||
columns: SpreadsheetColumns<T>,
|
||||
columnIndex: number,
|
||||
) => React.ReactNode;
|
||||
renderTemplateColumn: (
|
||||
columns: Columns<T>,
|
||||
columns: SpreadsheetColumns<T>,
|
||||
columnIndex: number,
|
||||
) => React.ReactNode;
|
||||
renderUnmatchedColumn: (
|
||||
columns: Columns<T>,
|
||||
columns: SpreadsheetColumns<T>,
|
||||
columnIndex: number,
|
||||
) => React.ReactNode;
|
||||
};
|
||||
|
||||
@ -2,20 +2,19 @@ import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import { SelectOption } from '@/spreadsheet-import/types';
|
||||
|
||||
import { getFieldOptions } from '@/spreadsheet-import/utils/getFieldOptions';
|
||||
|
||||
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 { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { IconChevronDown, Tag, TagColor } from 'twenty-ui';
|
||||
import {
|
||||
MatchedOptions,
|
||||
MatchedSelectColumn,
|
||||
MatchedSelectOptionsColumn,
|
||||
} from '../MatchColumnsStep';
|
||||
import { IconChevronDown, SelectOption, Tag, TagColor } from 'twenty-ui';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
@ -58,11 +57,15 @@ const StyledIconChevronDown = styled(IconChevronDown)`
|
||||
`;
|
||||
|
||||
interface SubMatchingSelectProps<T> {
|
||||
option: MatchedOptions<T> | Partial<MatchedOptions<T>>;
|
||||
column: MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T>;
|
||||
option: SpreadsheetMatchedOptions<T> | Partial<SpreadsheetMatchedOptions<T>>;
|
||||
column:
|
||||
| SpreadsheetMatchedSelectColumn<T>
|
||||
| SpreadsheetMatchedSelectOptionsColumn<T>;
|
||||
onSubChange: (val: T, index: number, option: string) => void;
|
||||
placeholder: string;
|
||||
selectedOption?: MatchedOptions<T> | Partial<MatchedOptions<T>>;
|
||||
selectedOption?:
|
||||
| SpreadsheetMatchedOptions<T>
|
||||
| Partial<SpreadsheetMatchedOptions<T>>;
|
||||
}
|
||||
|
||||
export const SubMatchingSelect = <T extends string>({
|
||||
|
||||
@ -3,9 +3,10 @@ import { IconForbid } from 'twenty-ui';
|
||||
|
||||
import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
|
||||
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 { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { Columns, ColumnType } from '../MatchColumnsStep';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
@ -15,7 +16,7 @@ const StyledContainer = styled.div`
|
||||
`;
|
||||
|
||||
type TemplateColumnProps<T extends string> = {
|
||||
columns: Columns<string>;
|
||||
columns: SpreadsheetColumns<string>;
|
||||
columnIndex: number;
|
||||
onChange: (val: T, index: number) => void;
|
||||
};
|
||||
@ -27,13 +28,13 @@ export const TemplateColumn = <T extends string>({
|
||||
}: TemplateColumnProps<T>) => {
|
||||
const { fields } = useSpreadsheetImportInternal<T>();
|
||||
const column = columns[columnIndex];
|
||||
const isIgnored = column.type === ColumnType.ignored;
|
||||
const isIgnored = column.type === SpreadsheetColumnType.ignored;
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
const fieldOptions = fields
|
||||
.filter((field) => field.fieldMetadataType !== FieldMetadataType.RICH_TEXT)
|
||||
.map(({ icon, label, key }) => {
|
||||
.map(({ Icon, label, key }) => {
|
||||
const isSelected =
|
||||
columns.findIndex((column) => {
|
||||
if ('value' in column) {
|
||||
@ -43,7 +44,7 @@ export const TemplateColumn = <T extends string>({
|
||||
}) !== -1;
|
||||
|
||||
return {
|
||||
icon: icon,
|
||||
Icon: Icon,
|
||||
value: key,
|
||||
label: label,
|
||||
disabled: isSelected,
|
||||
@ -52,7 +53,7 @@ export const TemplateColumn = <T extends string>({
|
||||
|
||||
const selectOptions = [
|
||||
{
|
||||
icon: IconForbid,
|
||||
Icon: IconForbid,
|
||||
value: 'do-not-import',
|
||||
label: t`Do not import`,
|
||||
},
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import { SubMatchingSelect } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelect';
|
||||
import { UnmatchColumnBanner } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumnBanner';
|
||||
import { Column } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { Fields } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
@ -10,8 +11,8 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui';
|
||||
|
||||
const getExpandableContainerTitle = <T extends string>(
|
||||
fields: Fields<T>,
|
||||
column: Column<T>,
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
column: SpreadsheetColumn<T>,
|
||||
) => {
|
||||
const fieldLabel = fields.find(
|
||||
(field) => 'value' in column && field.key === column.value,
|
||||
@ -24,7 +25,7 @@ const getExpandableContainerTitle = <T extends string>(
|
||||
};
|
||||
|
||||
type UnmatchColumnProps<T extends string> = {
|
||||
columns: Column<T>[];
|
||||
columns: SpreadsheetColumns<T>;
|
||||
columnIndex: number;
|
||||
onSubChange: (val: T, index: number, option: string) => void;
|
||||
};
|
||||
|
||||
@ -2,7 +2,7 @@ import styled from '@emotion/styled';
|
||||
|
||||
import { ImportedRow } from '@/spreadsheet-import/types';
|
||||
|
||||
import { Column } from '../MatchColumnsStep';
|
||||
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -30,7 +30,7 @@ const StyledExample = styled.span`
|
||||
`;
|
||||
|
||||
type UserTableColumnProps<T extends string> = {
|
||||
column: Column<T>;
|
||||
column: SpreadsheetColumn<T>;
|
||||
importedRow: ImportedRow;
|
||||
};
|
||||
|
||||
|
||||
@ -1,34 +1,32 @@
|
||||
import {
|
||||
Columns,
|
||||
ColumnType,
|
||||
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
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';
|
||||
|
||||
export const matchColumnsState = atom({
|
||||
key: 'MatchColumnsState',
|
||||
default: [] as Columns<string>,
|
||||
default: [] as SpreadsheetColumns<string>,
|
||||
});
|
||||
|
||||
export const initialComputedColumnsSelector = selectorFamily<
|
||||
Columns<string>,
|
||||
SpreadsheetColumns<string>,
|
||||
ImportedRow
|
||||
>({
|
||||
key: 'initialComputedColumnsSelector',
|
||||
get:
|
||||
(headerValues: ImportedRow) =>
|
||||
({ get }) => {
|
||||
const currentState = get(matchColumnsState) as Columns<string>;
|
||||
const currentState = get(matchColumnsState) as SpreadsheetColumns<string>;
|
||||
if (currentState.length === 0) {
|
||||
// Do not remove spread, it indexes empty array elements, otherwise map() skips over them
|
||||
const initialState = ([...headerValues] as string[]).map(
|
||||
(value, index) => ({
|
||||
type: ColumnType.empty,
|
||||
type: SpreadsheetColumnType.empty,
|
||||
index,
|
||||
header: value ?? '',
|
||||
}),
|
||||
);
|
||||
return initialState as Columns<string>;
|
||||
return initialState as SpreadsheetColumns<string>;
|
||||
} else {
|
||||
return currentState;
|
||||
}
|
||||
@ -36,6 +34,6 @@ export const initialComputedColumnsSelector = selectorFamily<
|
||||
set:
|
||||
() =>
|
||||
({ set }, newValue) => {
|
||||
set(matchColumnsState, newValue as Columns<string>);
|
||||
set(matchColumnsState, newValue as SpreadsheetColumns<string>);
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
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 { generateColumns } from './columns';
|
||||
|
||||
interface ExampleTableProps<T extends string> {
|
||||
fields: Fields<T>;
|
||||
fields: SpreadsheetImportFields<T>;
|
||||
}
|
||||
|
||||
export const ExampleTable = <T extends string>({
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import styled from '@emotion/styled';
|
||||
// @ts-expect-error // Todo: remove usage of react-data-grid
|
||||
import { Column } from 'react-data-grid';
|
||||
import { createPortal } from 'react-dom';
|
||||
import styled from '@emotion/styled';
|
||||
import { AppTooltip } from 'twenty-ui';
|
||||
|
||||
import { Fields } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||
|
||||
const StyledHeaderContainer = styled.div`
|
||||
align-items: center;
|
||||
@ -27,7 +27,9 @@ const StyledDefaultContainer = styled.div`
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
export const generateColumns = <T extends string>(fields: Fields<T>) =>
|
||||
export const generateColumns = <T extends string>(
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
) =>
|
||||
fields.map(
|
||||
(column): Column<any> => ({
|
||||
key: column.key,
|
||||
|
||||
@ -2,16 +2,14 @@ import { Heading } from '@/spreadsheet-import/components/Heading';
|
||||
import { SpreadsheetImportTable } from '@/spreadsheet-import/components/SpreadsheetImportTable';
|
||||
import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton';
|
||||
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 { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType';
|
||||
import {
|
||||
ImportValidationResult,
|
||||
ImportedStructuredRow,
|
||||
SpreadsheetImportImportValidationResult,
|
||||
} 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 { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager';
|
||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||
@ -74,7 +72,7 @@ const StyledNoRowsContainer = styled.div`
|
||||
|
||||
type ValidationStepProps<T extends string> = {
|
||||
initialData: ImportedStructuredRow<T>[];
|
||||
importedColumns: Columns<string>;
|
||||
importedColumns: SpreadsheetColumns<string>;
|
||||
file: File;
|
||||
onBack: () => void;
|
||||
setCurrentStepState: Dispatch<SetStateAction<SpreadsheetImportStep>>;
|
||||
@ -153,13 +151,14 @@ export const ValidationStep = <T extends string>({
|
||||
const hasBeenImported =
|
||||
importedColumns.filter(
|
||||
(importColumn) =>
|
||||
(importColumn.type === ColumnType.matched &&
|
||||
(importColumn.type === SpreadsheetColumnType.matched &&
|
||||
importColumn.value === column.key) ||
|
||||
(importColumn.type === ColumnType.matchedSelect &&
|
||||
(importColumn.type === SpreadsheetColumnType.matchedSelect &&
|
||||
importColumn.value === column.key) ||
|
||||
(importColumn.type === ColumnType.matchedSelectOptions &&
|
||||
(importColumn.type ===
|
||||
SpreadsheetColumnType.matchedSelectOptions &&
|
||||
importColumn.value === column.key) ||
|
||||
(importColumn.type === ColumnType.matchedCheckbox &&
|
||||
(importColumn.type === SpreadsheetColumnType.matchedCheckbox &&
|
||||
importColumn.value === column.key) ||
|
||||
column.key === 'select-row',
|
||||
).length > 0;
|
||||
@ -214,7 +213,7 @@ export const ValidationStep = <T extends string>({
|
||||
validStructuredRows: [] as ImportedStructuredRow<T>[],
|
||||
invalidStructuredRows: [] as ImportedStructuredRow<T>[],
|
||||
allStructuredRows: data,
|
||||
} satisfies ImportValidationResult<T>,
|
||||
} satisfies SpreadsheetImportImportValidationResult<T>,
|
||||
);
|
||||
|
||||
setCurrentStepState({
|
||||
|
||||
@ -5,11 +5,14 @@ import { createPortal } from 'react-dom';
|
||||
import { AppTooltip, Checkbox, CheckboxVariant, Toggle } from 'twenty-ui';
|
||||
|
||||
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 { ImportedStructuredRowMetadata } from '../types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ImportedStructuredRowMetadata } from '../types';
|
||||
|
||||
const StyledHeaderContainer = styled.div`
|
||||
align-items: center;
|
||||
@ -60,7 +63,7 @@ const StyledDefaultContainer = styled.div`
|
||||
const SELECT_COLUMN_KEY = 'select-row';
|
||||
|
||||
export const generateColumns = <T extends string>(
|
||||
fields: Fields<T>,
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
): Column<ImportedStructuredRow<T> & ImportedStructuredRowMetadata>[] => [
|
||||
{
|
||||
key: SELECT_COLUMN_KEY,
|
||||
@ -135,7 +138,7 @@ export const generateColumns = <T extends string>(
|
||||
value={
|
||||
value
|
||||
? ({
|
||||
icon: undefined,
|
||||
Icon: undefined,
|
||||
...value,
|
||||
} as const)
|
||||
: value
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { Info } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetImportInfo } from '@/spreadsheet-import/types';
|
||||
|
||||
export type ImportedStructuredRowMetadata = {
|
||||
__index: string;
|
||||
__errors?: Error | null;
|
||||
};
|
||||
export type Error = { [key: string]: Info };
|
||||
export type Error = { [key: string]: SpreadsheetImportInfo };
|
||||
export type Errors = { [id: string]: Error };
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Columns } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType';
|
||||
import { ImportedRow } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||
import { WorkBook } from 'xlsx-ugnis';
|
||||
|
||||
export type SpreadsheetImportStep =
|
||||
@ -23,7 +23,7 @@ export type SpreadsheetImportStep =
|
||||
| {
|
||||
type: SpreadsheetImportStepType.validateData;
|
||||
data: any[];
|
||||
importedColumns: Columns<string>;
|
||||
importedColumns: SpreadsheetColumns<string>;
|
||||
}
|
||||
| {
|
||||
type: SpreadsheetImportStepType.loading;
|
||||
|
||||
@ -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>;
|
||||
@ -0,0 +1,8 @@
|
||||
export enum SpreadsheetColumnType {
|
||||
empty,
|
||||
ignored,
|
||||
matched,
|
||||
matchedCheckbox,
|
||||
matchedSelect,
|
||||
matchedSelectOptions,
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||
|
||||
export type SpreadsheetColumns<T extends string> = SpreadsheetColumn<T>[];
|
||||
@ -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;
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export type SpreadsheetImportErrorLevel = 'info' | 'warning' | 'error';
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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>[]
|
||||
>;
|
||||
@ -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)[];
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export type ImportedRow = Array<string | undefined>;
|
||||
@ -0,0 +1,3 @@
|
||||
export type ImportedStructuredRow<T extends string> = {
|
||||
[key in T]: string | boolean | undefined;
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
import { SpreadsheetImportErrorLevel } from './SpreadsheetImportErrorLevel';
|
||||
|
||||
export type SpreadsheetImportInfo = {
|
||||
message: string;
|
||||
level: SpreadsheetImportErrorLevel;
|
||||
};
|
||||
@ -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>;
|
||||
@ -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>[];
|
||||
@ -0,0 +1,4 @@
|
||||
export type SpreadsheetMatchedOptions<T> = {
|
||||
entry: string;
|
||||
value?: T;
|
||||
};
|
||||
@ -1,197 +1,27 @@
|
||||
import { IconComponent, ThemeColor } from 'twenty-ui';
|
||||
import { ReadonlyDeep } from 'type-fest';
|
||||
// Import all types we need for re-export or alias
|
||||
|
||||
import { Columns } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { ImportedStructuredRowMetadata } from '@/spreadsheet-import/steps/components/ValidationStep/types';
|
||||
import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
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: Fields<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: Columns<FieldNames>,
|
||||
) => Promise<ImportedStructuredRow<FieldNames>[]>;
|
||||
// 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)[];
|
||||
};
|
||||
export type { SpreadsheetImportDialogOptions } from './SpreadsheetImportDialogOptions';
|
||||
export type { SpreadsheetImportErrorLevel } from './SpreadsheetImportErrorLevel';
|
||||
export type { SpreadsheetImportField } from './SpreadsheetImportField';
|
||||
export type { SpreadsheetImportFields } from './SpreadsheetImportFields';
|
||||
export type {
|
||||
SpreadsheetImportCheckbox,
|
||||
SpreadsheetImportFieldType,
|
||||
SpreadsheetImportInput,
|
||||
SpreadsheetImportMultiSelect,
|
||||
SpreadsheetImportSelect,
|
||||
} from './SpreadsheetImportFieldType';
|
||||
export type {
|
||||
SpreadsheetImportFieldValidationDefinition,
|
||||
SpreadsheetImportFunctionValidation,
|
||||
SpreadsheetImportObjectValidation,
|
||||
SpreadsheetImportRegexValidation,
|
||||
SpreadsheetImportRequiredValidation,
|
||||
SpreadsheetImportUniqueValidation,
|
||||
} from './SpreadsheetImportFieldValidationDefinition';
|
||||
export type { ImportedRow } from './SpreadsheetImportImportedRow';
|
||||
export type { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow';
|
||||
export type { SpreadsheetImportImportValidationResult } from './SpreadsheetImportImportValidationResult';
|
||||
export type { SpreadsheetImportInfo } from './SpreadsheetImportInfo';
|
||||
export type { SpreadsheetImportRowHook } from './SpreadsheetImportRowHook';
|
||||
export type { SpreadsheetImportTableHook } from './SpreadsheetImportTableHook';
|
||||
|
||||
@ -1,45 +1,45 @@
|
||||
import {
|
||||
Field,
|
||||
ImportedStructuredRow,
|
||||
Info,
|
||||
RowHook,
|
||||
TableHook,
|
||||
SpreadsheetImportField,
|
||||
SpreadsheetImportInfo,
|
||||
SpreadsheetImportRowHook,
|
||||
SpreadsheetImportTableHook,
|
||||
} from '@/spreadsheet-import/types';
|
||||
import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
describe('addErrorsAndRunHooks', () => {
|
||||
type FullData = ImportedStructuredRow<'name' | 'age' | 'country'>;
|
||||
const requiredField: Field<'name'> = {
|
||||
const requiredField: SpreadsheetImportField<'name'> = {
|
||||
key: 'name',
|
||||
label: 'Name',
|
||||
fieldValidationDefinitions: [{ rule: 'required' }],
|
||||
icon: null,
|
||||
Icon: null,
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
};
|
||||
|
||||
const regexField: Field<'age'> = {
|
||||
const regexField: SpreadsheetImportField<'age'> = {
|
||||
key: 'age',
|
||||
label: 'Age',
|
||||
fieldValidationDefinitions: [
|
||||
{ rule: 'regex', value: '\\d+', errorMessage: 'Regex error' },
|
||||
],
|
||||
icon: null,
|
||||
Icon: null,
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.NUMBER,
|
||||
};
|
||||
|
||||
const uniqueField: Field<'country'> = {
|
||||
const uniqueField: SpreadsheetImportField<'country'> = {
|
||||
key: 'country',
|
||||
label: 'Country',
|
||||
fieldValidationDefinitions: [{ rule: 'unique' }],
|
||||
icon: null,
|
||||
Icon: null,
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.SELECT,
|
||||
};
|
||||
|
||||
const functionValidationFieldTrue: Field<'email'> = {
|
||||
const functionValidationFieldTrue: SpreadsheetImportField<'email'> = {
|
||||
key: 'email',
|
||||
label: 'Email',
|
||||
fieldValidationDefinitions: [
|
||||
@ -49,12 +49,12 @@ describe('addErrorsAndRunHooks', () => {
|
||||
errorMessage: 'Field is invalid',
|
||||
},
|
||||
],
|
||||
icon: null,
|
||||
Icon: null,
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.EMAILS,
|
||||
};
|
||||
|
||||
const functionValidationFieldFalse: Field<'email'> = {
|
||||
const functionValidationFieldFalse: SpreadsheetImportField<'email'> = {
|
||||
key: 'email',
|
||||
label: 'Email',
|
||||
fieldValidationDefinitions: [
|
||||
@ -64,7 +64,7 @@ describe('addErrorsAndRunHooks', () => {
|
||||
errorMessage: 'Field is invalid',
|
||||
},
|
||||
],
|
||||
icon: null,
|
||||
Icon: null,
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.EMAILS,
|
||||
};
|
||||
@ -88,24 +88,43 @@ describe('addErrorsAndRunHooks', () => {
|
||||
dataWithoutNameAndInvalidAge,
|
||||
];
|
||||
|
||||
const basicError: Info = { message: 'Field is invalid', level: 'error' };
|
||||
const nameError: Info = { message: 'Name Error', level: 'error' };
|
||||
const ageError: Info = { message: 'Age Error', level: 'error' };
|
||||
const regexError: Info = { message: 'Regex error', level: 'error' };
|
||||
const requiredError: Info = { message: 'Field is required', level: 'error' };
|
||||
const duplicatedError: Info = {
|
||||
const basicError: SpreadsheetImportInfo = {
|
||||
message: 'Field is invalid',
|
||||
level: 'error',
|
||||
};
|
||||
const nameError: SpreadsheetImportInfo = {
|
||||
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',
|
||||
level: 'error',
|
||||
};
|
||||
|
||||
const rowHook: RowHook<'name' | 'age'> = jest.fn((row, addError) => {
|
||||
addError('name', nameError);
|
||||
return row;
|
||||
});
|
||||
const tableHook: TableHook<'name' | 'age'> = jest.fn((table, addError) => {
|
||||
addError(0, 'age', ageError);
|
||||
return table;
|
||||
});
|
||||
const rowHook: SpreadsheetImportRowHook<'name' | 'age'> = jest.fn(
|
||||
(row, addError) => {
|
||||
addError('name', nameError);
|
||||
return row;
|
||||
},
|
||||
);
|
||||
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', () => {
|
||||
const result = addErrorsAndRunHooks(
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { Field } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetImportField } from '@/spreadsheet-import/types';
|
||||
import { findMatch } from '@/spreadsheet-import/utils/findMatch';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
describe('findMatch', () => {
|
||||
const defaultField: Field<'defaultField'> = {
|
||||
const defaultField: SpreadsheetImportField<'defaultField'> = {
|
||||
key: 'defaultField',
|
||||
icon: null,
|
||||
Icon: null,
|
||||
label: 'label',
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
@ -14,9 +14,9 @@ describe('findMatch', () => {
|
||||
alternateMatches: ['Full Name', 'First Name'],
|
||||
};
|
||||
|
||||
const secondaryField: Field<'secondaryField'> = {
|
||||
const secondaryField: SpreadsheetImportField<'secondaryField'> = {
|
||||
key: 'secondaryField',
|
||||
icon: null,
|
||||
Icon: null,
|
||||
label: 'label',
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
|
||||
@ -1,59 +1,62 @@
|
||||
import {
|
||||
Column,
|
||||
ColumnType,
|
||||
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { Field, FieldValidationDefinition } from '@/spreadsheet-import/types';
|
||||
SpreadsheetImportField,
|
||||
SpreadsheetImportFieldValidationDefinition,
|
||||
} 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 { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
const nameField: Field<'Name'> = {
|
||||
const nameField: SpreadsheetImportField<'Name'> = {
|
||||
key: 'Name',
|
||||
label: 'Name',
|
||||
icon: null,
|
||||
Icon: null,
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
};
|
||||
|
||||
const ageField: Field<'Age'> = {
|
||||
const ageField: SpreadsheetImportField<'Age'> = {
|
||||
key: 'Age',
|
||||
label: 'Age',
|
||||
icon: null,
|
||||
Icon: null,
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
fieldMetadataType: FieldMetadataType.NUMBER,
|
||||
};
|
||||
|
||||
const validations: FieldValidationDefinition[] = [{ rule: 'required' }];
|
||||
const nameFieldWithValidations: Field<'Name'> = {
|
||||
const validations: SpreadsheetImportFieldValidationDefinition[] = [
|
||||
{ rule: 'required' },
|
||||
];
|
||||
const nameFieldWithValidations: SpreadsheetImportField<'Name'> = {
|
||||
...nameField,
|
||||
fieldValidationDefinitions: validations,
|
||||
};
|
||||
const ageFieldWithValidations: Field<'Age'> = {
|
||||
const ageFieldWithValidations: SpreadsheetImportField<'Age'> = {
|
||||
...ageField,
|
||||
fieldValidationDefinitions: validations,
|
||||
};
|
||||
|
||||
type ColumnValues = 'Name' | 'Age';
|
||||
|
||||
const nameColumn: Column<ColumnValues> = {
|
||||
type: ColumnType.matched,
|
||||
const nameColumn: SpreadsheetColumn<ColumnValues> = {
|
||||
type: SpreadsheetColumnType.matched,
|
||||
index: 0,
|
||||
header: '',
|
||||
value: 'Name',
|
||||
};
|
||||
|
||||
const ageColumn: Column<ColumnValues> = {
|
||||
type: ColumnType.matched,
|
||||
const ageColumn: SpreadsheetColumn<ColumnValues> = {
|
||||
type: SpreadsheetColumnType.matched,
|
||||
index: 0,
|
||||
header: '',
|
||||
value: 'Age',
|
||||
};
|
||||
|
||||
const extraColumn: Column<ColumnValues> = {
|
||||
type: ColumnType.matched,
|
||||
const extraColumn: SpreadsheetColumn<ColumnValues> = {
|
||||
type: SpreadsheetColumnType.matched,
|
||||
index: 0,
|
||||
header: '',
|
||||
value: 'Age',
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { Field } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetImportField } from '@/spreadsheet-import/types';
|
||||
import { generateExampleRow } from '@/spreadsheet-import/utils/generateExampleRow';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
describe('generateExampleRow', () => {
|
||||
const defaultField: Field<'defaultField'> = {
|
||||
const defaultField: SpreadsheetImportField<'defaultField'> = {
|
||||
key: 'defaultField',
|
||||
icon: null,
|
||||
Icon: null,
|
||||
label: 'label',
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
@ -14,7 +14,7 @@ describe('generateExampleRow', () => {
|
||||
};
|
||||
|
||||
it('should generate an example row from input field type', () => {
|
||||
const fields: Field<'defaultField'>[] = [defaultField];
|
||||
const fields: SpreadsheetImportField<'defaultField'>[] = [defaultField];
|
||||
|
||||
const result = generateExampleRow(fields);
|
||||
|
||||
@ -22,7 +22,7 @@ describe('generateExampleRow', () => {
|
||||
});
|
||||
|
||||
it('should generate an example row from checkbox field type', () => {
|
||||
const fields: Field<'defaultField'>[] = [
|
||||
const fields: SpreadsheetImportField<'defaultField'>[] = [
|
||||
{
|
||||
...defaultField,
|
||||
fieldType: { type: 'checkbox' },
|
||||
@ -36,7 +36,7 @@ describe('generateExampleRow', () => {
|
||||
});
|
||||
|
||||
it('should generate an example row from select field type', () => {
|
||||
const fields: Field<'defaultField'>[] = [
|
||||
const fields: SpreadsheetImportField<'defaultField'>[] = [
|
||||
{
|
||||
...defaultField,
|
||||
fieldType: { type: 'select', options: [] },
|
||||
@ -50,7 +50,7 @@ describe('generateExampleRow', () => {
|
||||
});
|
||||
|
||||
it('should generate an example row with provided example values for fields', () => {
|
||||
const fields: Field<'defaultField'>[] = [
|
||||
const fields: SpreadsheetImportField<'defaultField'>[] = [
|
||||
{
|
||||
...defaultField,
|
||||
example: 'Example',
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Field } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetImportField } from '@/spreadsheet-import/types';
|
||||
import { getFieldOptions } from '@/spreadsheet-import/utils/getFieldOptions';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
@ -17,10 +17,10 @@ describe('getFieldOptions', () => {
|
||||
value: 'Three',
|
||||
},
|
||||
];
|
||||
const fields: Field<'Options' | 'Name'>[] = [
|
||||
const fields: SpreadsheetImportField<'Options' | 'Name'>[] = [
|
||||
{
|
||||
key: 'Options',
|
||||
icon: null,
|
||||
Icon: null,
|
||||
label: 'options',
|
||||
fieldType: {
|
||||
type: 'select',
|
||||
@ -30,7 +30,7 @@ describe('getFieldOptions', () => {
|
||||
},
|
||||
{
|
||||
key: 'Name',
|
||||
icon: null,
|
||||
Icon: null,
|
||||
label: 'name',
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
|
||||
@ -1,49 +1,52 @@
|
||||
import {
|
||||
Column,
|
||||
ColumnType,
|
||||
} 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 { getMatchedColumns } from '@/spreadsheet-import/utils/getMatchedColumns';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
describe('getMatchedColumns', () => {
|
||||
const columns: Column<string>[] = [
|
||||
{ index: 0, header: 'Name', type: ColumnType.matched, value: 'Name' },
|
||||
const columns: SpreadsheetColumn<string>[] = [
|
||||
{
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'Name',
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
header: 'Location',
|
||||
type: ColumnType.matched,
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'Location',
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
header: 'Age',
|
||||
type: ColumnType.matched,
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'Age',
|
||||
},
|
||||
];
|
||||
|
||||
const fields: Field<string>[] = [
|
||||
const fields: SpreadsheetImportField<string>[] = [
|
||||
{
|
||||
key: 'Name',
|
||||
label: 'Name',
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
icon: null,
|
||||
Icon: null,
|
||||
},
|
||||
{
|
||||
key: 'Location',
|
||||
label: 'Location',
|
||||
fieldType: { type: 'select', options: [] },
|
||||
fieldMetadataType: FieldMetadataType.POSITION,
|
||||
icon: null,
|
||||
Icon: null,
|
||||
},
|
||||
{
|
||||
key: 'Age',
|
||||
label: 'Age',
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.NUMBER,
|
||||
icon: null,
|
||||
Icon: null,
|
||||
},
|
||||
];
|
||||
|
||||
@ -57,11 +60,16 @@ describe('getMatchedColumns', () => {
|
||||
it('should return matched columns for each field', () => {
|
||||
const result = getMatchedColumns(columns, fields, data, autoMapDistance);
|
||||
expect(result).toEqual([
|
||||
{ index: 0, header: 'Name', type: ColumnType.matched, value: 'Name' },
|
||||
{
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'Name',
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
header: 'Location',
|
||||
type: ColumnType.matchedSelect,
|
||||
type: SpreadsheetColumnType.matchedSelect,
|
||||
value: 'Location',
|
||||
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', () => {
|
||||
const columnsWithDuplicates: Column<string>[] = [
|
||||
{ index: 0, header: 'Name', type: ColumnType.matched, value: 'Name' },
|
||||
{ index: 1, header: 'Name', type: ColumnType.matched, value: 'Name' },
|
||||
const columnsWithDuplicates: SpreadsheetColumn<string>[] = [
|
||||
{
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'Name',
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
header: 'Name',
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'Name',
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
header: 'Location',
|
||||
type: ColumnType.matched,
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'Location',
|
||||
},
|
||||
];
|
||||
@ -98,12 +121,12 @@ describe('getMatchedColumns', () => {
|
||||
expect(result[0]).toEqual({
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: ColumnType.empty,
|
||||
type: SpreadsheetColumnType.empty,
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
index: 1,
|
||||
header: 'Name',
|
||||
type: ColumnType.matched,
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'Name',
|
||||
});
|
||||
});
|
||||
@ -114,20 +137,20 @@ describe('getMatchedColumns', () => {
|
||||
['Alice', 'Los Angeles', '25'],
|
||||
];
|
||||
|
||||
const unmatchedFields: Field<string>[] = [
|
||||
const unmatchedFields: SpreadsheetImportField<string>[] = [
|
||||
{
|
||||
key: 'Hobby',
|
||||
label: 'Hobby',
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
icon: null,
|
||||
Icon: null,
|
||||
},
|
||||
{
|
||||
key: 'Interest',
|
||||
label: 'Interest',
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
icon: null,
|
||||
Icon: null,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -1,37 +1,46 @@
|
||||
import {
|
||||
Column,
|
||||
ColumnType,
|
||||
} 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 { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
||||
import { normalizeTableData } from '@/spreadsheet-import/utils/normalizeTableData';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
describe('normalizeTableData', () => {
|
||||
const columns: Column<string>[] = [
|
||||
{ index: 0, header: 'Name', type: ColumnType.matched, value: 'name' },
|
||||
{ index: 1, header: 'Age', type: ColumnType.matched, value: 'age' },
|
||||
const columns: SpreadsheetColumn<string>[] = [
|
||||
{
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'name',
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
header: 'Age',
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'age',
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
header: 'Active',
|
||||
type: ColumnType.matchedCheckbox,
|
||||
type: SpreadsheetColumnType.matchedCheckbox,
|
||||
value: 'active',
|
||||
},
|
||||
];
|
||||
|
||||
const fields: Field<string>[] = [
|
||||
const fields: SpreadsheetImportField<string>[] = [
|
||||
{
|
||||
key: 'name',
|
||||
label: 'Name',
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
icon: null,
|
||||
Icon: null,
|
||||
},
|
||||
{
|
||||
key: 'age',
|
||||
label: 'Age',
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.NUMBER,
|
||||
icon: null,
|
||||
Icon: null,
|
||||
},
|
||||
{
|
||||
key: 'active',
|
||||
@ -40,7 +49,7 @@ describe('normalizeTableData', () => {
|
||||
type: 'checkbox',
|
||||
},
|
||||
fieldMetadataType: FieldMetadataType.BOOLEAN,
|
||||
icon: null,
|
||||
Icon: null,
|
||||
},
|
||||
];
|
||||
|
||||
@ -61,16 +70,16 @@ describe('normalizeTableData', () => {
|
||||
});
|
||||
|
||||
it('should normalize matchedCheckbox values and handle booleanMatches', () => {
|
||||
const columns: Column<string>[] = [
|
||||
const columns: SpreadsheetColumn<string>[] = [
|
||||
{
|
||||
index: 0,
|
||||
header: 'Active',
|
||||
type: ColumnType.matchedCheckbox,
|
||||
type: SpreadsheetColumnType.matchedCheckbox,
|
||||
value: 'active',
|
||||
},
|
||||
];
|
||||
|
||||
const fields: Field<string>[] = [
|
||||
const fields: SpreadsheetImportField<string>[] = [
|
||||
{
|
||||
key: 'active',
|
||||
label: 'Active',
|
||||
@ -79,7 +88,7 @@ describe('normalizeTableData', () => {
|
||||
booleanMatches: { yes: true, no: false },
|
||||
},
|
||||
fieldMetadataType: FieldMetadataType.BOOLEAN,
|
||||
icon: null,
|
||||
Icon: null,
|
||||
},
|
||||
];
|
||||
|
||||
@ -91,11 +100,11 @@ describe('normalizeTableData', () => {
|
||||
});
|
||||
|
||||
it('should map matchedSelect and matchedSelectOptions values correctly', () => {
|
||||
const columns: Column<string>[] = [
|
||||
const columns: SpreadsheetColumn<string>[] = [
|
||||
{
|
||||
index: 0,
|
||||
header: 'Number',
|
||||
type: ColumnType.matchedSelect,
|
||||
type: SpreadsheetColumnType.matchedSelect,
|
||||
value: 'number',
|
||||
matchedOptions: [
|
||||
{ entry: 'One', value: '1' },
|
||||
@ -104,7 +113,7 @@ describe('normalizeTableData', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const fields: Field<string>[] = [
|
||||
const fields: SpreadsheetImportField<string>[] = [
|
||||
{
|
||||
key: 'number',
|
||||
label: 'Number',
|
||||
@ -116,7 +125,7 @@ describe('normalizeTableData', () => {
|
||||
],
|
||||
},
|
||||
fieldMetadataType: FieldMetadataType.SELECT,
|
||||
icon: null,
|
||||
Icon: null,
|
||||
},
|
||||
];
|
||||
|
||||
@ -132,9 +141,9 @@ describe('normalizeTableData', () => {
|
||||
});
|
||||
|
||||
it('should handle empty and ignored columns', () => {
|
||||
const columns: Column<string>[] = [
|
||||
{ index: 0, header: 'Empty', type: ColumnType.empty },
|
||||
{ index: 1, header: 'Ignored', type: ColumnType.ignored },
|
||||
const columns: SpreadsheetColumn<string>[] = [
|
||||
{ index: 0, header: 'Empty', type: SpreadsheetColumnType.empty },
|
||||
{ index: 1, header: 'Ignored', type: SpreadsheetColumnType.ignored },
|
||||
];
|
||||
|
||||
const rawData = [['Value1', 'Value2']];
|
||||
@ -145,11 +154,11 @@ describe('normalizeTableData', () => {
|
||||
});
|
||||
|
||||
it('should handle unrecognized column types and return empty object', () => {
|
||||
const columns: Column<string>[] = [
|
||||
const columns: SpreadsheetColumns<string> = [
|
||||
{
|
||||
index: 0,
|
||||
header: 'Unrecognized',
|
||||
type: 'Unknown' as unknown as ColumnType.matched,
|
||||
type: 'Unknown' as unknown as SpreadsheetColumnType.matched,
|
||||
value: '',
|
||||
},
|
||||
];
|
||||
|
||||
@ -1,24 +1,22 @@
|
||||
import {
|
||||
Column,
|
||||
ColumnType,
|
||||
} 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 { setColumn } from '@/spreadsheet-import/utils/setColumn';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
describe('setColumn', () => {
|
||||
const defaultField: Field<'Name'> = {
|
||||
icon: null,
|
||||
const defaultField: SpreadsheetImportField<'Name'> = {
|
||||
Icon: null,
|
||||
label: 'label',
|
||||
key: 'Name',
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
};
|
||||
|
||||
const oldColumn: Column<'oldValue'> = {
|
||||
const oldColumn: SpreadsheetColumn<'oldValue'> = {
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: ColumnType.matched,
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'oldValue',
|
||||
};
|
||||
|
||||
@ -29,7 +27,7 @@ describe('setColumn', () => {
|
||||
type: 'select',
|
||||
options: [{ value: 'John' }, { value: 'Alice' }],
|
||||
},
|
||||
} as Field<'Name'>;
|
||||
} as SpreadsheetImportField<'Name'>;
|
||||
|
||||
const data = [['John'], ['Alice']];
|
||||
const result = setColumn(oldColumn, field, data);
|
||||
@ -37,7 +35,7 @@ describe('setColumn', () => {
|
||||
expect(result).toEqual({
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: ColumnType.matchedSelectOptions,
|
||||
type: SpreadsheetColumnType.matchedSelectOptions,
|
||||
value: 'Name',
|
||||
matchedOptions: [
|
||||
{
|
||||
@ -56,14 +54,14 @@ describe('setColumn', () => {
|
||||
const field = {
|
||||
...defaultField,
|
||||
fieldType: { type: 'checkbox' },
|
||||
} as Field<'Name'>;
|
||||
} as SpreadsheetImportField<'Name'>;
|
||||
|
||||
const result = setColumn(oldColumn, field);
|
||||
|
||||
expect(result).toEqual({
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: ColumnType.matchedCheckbox,
|
||||
type: SpreadsheetColumnType.matchedCheckbox,
|
||||
value: 'Name',
|
||||
});
|
||||
});
|
||||
@ -72,14 +70,14 @@ describe('setColumn', () => {
|
||||
const field = {
|
||||
...defaultField,
|
||||
fieldType: { type: 'input' },
|
||||
} as Field<'Name'>;
|
||||
} as SpreadsheetImportField<'Name'>;
|
||||
|
||||
const result = setColumn(oldColumn, field);
|
||||
|
||||
expect(result).toEqual({
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: ColumnType.matched,
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'Name',
|
||||
});
|
||||
});
|
||||
@ -88,14 +86,14 @@ describe('setColumn', () => {
|
||||
const field = {
|
||||
...defaultField,
|
||||
fieldType: { type: 'unknown' },
|
||||
} as unknown as Field<'Name'>;
|
||||
} as unknown as SpreadsheetImportField<'Name'>;
|
||||
|
||||
const result = setColumn(oldColumn, field);
|
||||
|
||||
expect(result).toEqual({
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: ColumnType.empty,
|
||||
type: SpreadsheetColumnType.empty,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,22 +1,20 @@
|
||||
import {
|
||||
Column,
|
||||
ColumnType,
|
||||
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
||||
import { setIgnoreColumn } from '@/spreadsheet-import/utils/setIgnoreColumn';
|
||||
|
||||
describe('setIgnoreColumn', () => {
|
||||
it('should return a column with type "ignored"', () => {
|
||||
const column: Column<'John'> = {
|
||||
const column: SpreadsheetColumn<'John'> = {
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: ColumnType.matched,
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'John',
|
||||
};
|
||||
const result = setIgnoreColumn(column);
|
||||
expect(result).toEqual({
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: ColumnType.ignored,
|
||||
type: SpreadsheetColumnType.ignored,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,15 +1,13 @@
|
||||
import {
|
||||
Column,
|
||||
ColumnType,
|
||||
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
||||
import { setSubColumn } from '@/spreadsheet-import/utils/setSubColumn';
|
||||
|
||||
describe('setSubColumn', () => {
|
||||
it('should return a matchedSelectColumn with updated matchedOptions', () => {
|
||||
const oldColumn: Column<'John' | ''> = {
|
||||
const oldColumn: SpreadsheetColumn<'John' | ''> = {
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: ColumnType.matchedSelect,
|
||||
type: SpreadsheetColumnType.matchedSelect,
|
||||
matchedOptions: [
|
||||
{ entry: 'Name1', value: 'John' },
|
||||
{ entry: 'Name2', value: '' },
|
||||
@ -24,7 +22,7 @@ describe('setSubColumn', () => {
|
||||
expect(result).toEqual({
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: ColumnType.matchedSelect,
|
||||
type: SpreadsheetColumnType.matchedSelect,
|
||||
matchedOptions: [
|
||||
{ entry: 'Name1', value: 'John Doe' },
|
||||
{ entry: 'Name2', value: '' },
|
||||
@ -34,10 +32,10 @@ describe('setSubColumn', () => {
|
||||
});
|
||||
|
||||
it('should return a matchedSelectOptionsColumn with updated matchedOptions', () => {
|
||||
const oldColumn: Column<'John' | 'Jane'> = {
|
||||
const oldColumn: SpreadsheetColumn<'John' | 'Jane'> = {
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: ColumnType.matchedSelectOptions,
|
||||
type: SpreadsheetColumnType.matchedSelectOptions,
|
||||
matchedOptions: [
|
||||
{ entry: 'Name1', value: 'John' },
|
||||
{ entry: 'Name2', value: 'Jane' },
|
||||
@ -52,7 +50,7 @@ describe('setSubColumn', () => {
|
||||
expect(result).toEqual({
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: ColumnType.matchedSelectOptions,
|
||||
type: SpreadsheetColumnType.matchedSelectOptions,
|
||||
matchedOptions: [
|
||||
{ entry: 'Name1', value: 'John Doe' },
|
||||
{ entry: 'Name2', value: 'Jane' },
|
||||
|
||||
@ -6,24 +6,28 @@ import {
|
||||
ImportedStructuredRowMetadata,
|
||||
} from '@/spreadsheet-import/steps/components/ValidationStep/types';
|
||||
import {
|
||||
Fields,
|
||||
ImportedStructuredRow,
|
||||
Info,
|
||||
RowHook,
|
||||
TableHook,
|
||||
SpreadsheetImportFields,
|
||||
SpreadsheetImportInfo,
|
||||
SpreadsheetImportRowHook,
|
||||
SpreadsheetImportTableHook,
|
||||
} from '@/spreadsheet-import/types';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export const addErrorsAndRunHooks = <T extends string>(
|
||||
data: (ImportedStructuredRow<T> & Partial<ImportedStructuredRowMetadata>)[],
|
||||
fields: Fields<T>,
|
||||
rowHook?: RowHook<T>,
|
||||
tableHook?: TableHook<T>,
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
rowHook?: SpreadsheetImportRowHook<T>,
|
||||
tableHook?: SpreadsheetImportTableHook<T>,
|
||||
): (ImportedStructuredRow<T> & ImportedStructuredRowMetadata)[] => {
|
||||
const errors: Errors = {};
|
||||
|
||||
const addHookError = (rowIndex: number, fieldKey: T, error: Info) => {
|
||||
const addHookError = (
|
||||
rowIndex: number,
|
||||
fieldKey: T,
|
||||
error: SpreadsheetImportInfo,
|
||||
) => {
|
||||
errors[rowIndex] = {
|
||||
...errors[rowIndex],
|
||||
[fieldKey]: error,
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||
import lavenstein from 'js-levenshtein';
|
||||
|
||||
import { Fields } from '@/spreadsheet-import/types';
|
||||
|
||||
type AutoMatchAccumulator<T> = {
|
||||
distance: number;
|
||||
value: T;
|
||||
@ -9,7 +8,7 @@ type AutoMatchAccumulator<T> = {
|
||||
|
||||
export const findMatch = <T extends string>(
|
||||
header: string,
|
||||
fields: Fields<T>,
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
autoMapDistance: number,
|
||||
): T | undefined => {
|
||||
const smallestValue = fields.reduce<AutoMatchAccumulator<T>>((acc, field) => {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { Columns } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { Fields } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||
|
||||
export const findUnmatchedRequiredFields = <T extends string>(
|
||||
fields: Fields<T>,
|
||||
columns: Columns<T>,
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
columns: SpreadsheetColumns<T>,
|
||||
) =>
|
||||
fields
|
||||
.filter((field) =>
|
||||
|
||||
@ -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',
|
||||
select: 'Options',
|
||||
multiSelect: 'Options',
|
||||
input: 'Text',
|
||||
};
|
||||
|
||||
export const generateExampleRow = <T extends string>(fields: Fields<T>) => [
|
||||
export const generateExampleRow = <T extends string>(
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
) => [
|
||||
fields.reduce(
|
||||
(acc, field) => {
|
||||
acc[field.key as T] = field.example || titleMap[field.fieldType.type];
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Fields } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||
|
||||
export const getFieldOptions = <T extends string>(
|
||||
fields: Fields<T>,
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
fieldKey: string,
|
||||
) => {
|
||||
const field = fields.find(({ key }) => fieldKey === key);
|
||||
|
||||
@ -1,26 +1,29 @@
|
||||
import lavenstein from 'js-levenshtein';
|
||||
|
||||
import {
|
||||
Column,
|
||||
Columns,
|
||||
MatchColumnsStepProps,
|
||||
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { Field, Fields } from '@/spreadsheet-import/types';
|
||||
import { MatchColumnsStepProps } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
|
||||
import {
|
||||
SpreadsheetImportField,
|
||||
SpreadsheetImportFields,
|
||||
} from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { findMatch } from './findMatch';
|
||||
import { setColumn } from './setColumn';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const getMatchedColumns = <T extends string>(
|
||||
columns: Columns<T>,
|
||||
fields: Fields<T>,
|
||||
columns: SpreadsheetColumns<T>,
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
data: MatchColumnsStepProps['data'],
|
||||
autoMapDistance: number,
|
||||
) =>
|
||||
columns.reduce<Column<T>[]>((arr, column) => {
|
||||
columns.reduce<SpreadsheetColumn<T>[]>((arr, column) => {
|
||||
const autoMatch = findMatch(column.header, fields, autoMapDistance);
|
||||
if (isDefined(autoMatch)) {
|
||||
const field = fields.find((field) => field.key === autoMatch) as Field<T>;
|
||||
const field = fields.find(
|
||||
(field) => field.key === autoMatch,
|
||||
) as SpreadsheetImportField<T>;
|
||||
const duplicateIndex = arr.findIndex(
|
||||
(column) => 'value' in column && column.value === field.key,
|
||||
);
|
||||
|
||||
@ -1,26 +1,24 @@
|
||||
import {
|
||||
Columns,
|
||||
ColumnType,
|
||||
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import {
|
||||
Fields,
|
||||
ImportedRow,
|
||||
ImportedStructuredRow,
|
||||
SpreadsheetImportFields,
|
||||
} 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 { normalizeCheckboxValue } from './normalizeCheckboxValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const normalizeTableData = <T extends string>(
|
||||
columns: Columns<T>,
|
||||
columns: SpreadsheetColumns<T>,
|
||||
data: ImportedRow[],
|
||||
fields: Fields<T>,
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
) =>
|
||||
data.map((row) =>
|
||||
columns.reduce((acc, column, index) => {
|
||||
const curr = row[index];
|
||||
switch (column.type) {
|
||||
case ColumnType.matchedCheckbox: {
|
||||
case SpreadsheetColumnType.matchedCheckbox: {
|
||||
const field = fields.find((field) => field.key === column.value);
|
||||
|
||||
if (!field) {
|
||||
@ -49,12 +47,12 @@ export const normalizeTableData = <T extends string>(
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
case ColumnType.matched: {
|
||||
case SpreadsheetColumnType.matched: {
|
||||
acc[column.value] = curr === '' ? undefined : curr;
|
||||
return acc;
|
||||
}
|
||||
case ColumnType.matchedSelect:
|
||||
case ColumnType.matchedSelectOptions: {
|
||||
case SpreadsheetColumnType.matchedSelect:
|
||||
case SpreadsheetColumnType.matchedSelectOptions: {
|
||||
const field = fields.find((field) => field.key === column.value);
|
||||
|
||||
if (!field) {
|
||||
@ -96,8 +94,8 @@ export const normalizeTableData = <T extends string>(
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
case ColumnType.empty:
|
||||
case ColumnType.ignored: {
|
||||
case SpreadsheetColumnType.empty:
|
||||
case SpreadsheetColumnType.ignored: {
|
||||
return acc;
|
||||
}
|
||||
default:
|
||||
|
||||
@ -1,25 +1,23 @@
|
||||
import {
|
||||
Column,
|
||||
ColumnType,
|
||||
MatchColumnsStepProps,
|
||||
MatchedOptions,
|
||||
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { Field } from '@/spreadsheet-import/types';
|
||||
import { MatchColumnsStepProps } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
|
||||
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 { uniqueEntries } from './uniqueEntries';
|
||||
|
||||
export const setColumn = <T extends string>(
|
||||
oldColumn: Column<T>,
|
||||
field?: Field<T>,
|
||||
oldColumn: SpreadsheetColumn<T>,
|
||||
field?: SpreadsheetImportField<T>,
|
||||
data?: MatchColumnsStepProps['data'],
|
||||
): Column<T> => {
|
||||
): SpreadsheetColumn<T> => {
|
||||
if (field?.fieldType.type === 'select') {
|
||||
const fieldOptions = field.fieldType.options;
|
||||
const uniqueData = uniqueEntries(
|
||||
data || [],
|
||||
oldColumn.index,
|
||||
) as MatchedOptions<T>[];
|
||||
) as SpreadsheetMatchedOptions<T>[];
|
||||
|
||||
const matchedOptions = uniqueData.map((record) => {
|
||||
const value = fieldOptions.find(
|
||||
@ -28,8 +26,8 @@ export const setColumn = <T extends string>(
|
||||
fieldOption.label === record.entry,
|
||||
)?.value;
|
||||
return value
|
||||
? ({ ...record, value } as MatchedOptions<T>)
|
||||
: (record as MatchedOptions<T>);
|
||||
? ({ ...record, value } as SpreadsheetMatchedOptions<T>)
|
||||
: (record as SpreadsheetMatchedOptions<T>);
|
||||
});
|
||||
const allMatched =
|
||||
matchedOptions.filter((o) => o.value).length === uniqueData?.length;
|
||||
@ -37,8 +35,8 @@ export const setColumn = <T extends string>(
|
||||
return {
|
||||
...oldColumn,
|
||||
type: allMatched
|
||||
? ColumnType.matchedSelectOptions
|
||||
: ColumnType.matchedSelect,
|
||||
? SpreadsheetColumnType.matchedSelectOptions
|
||||
: SpreadsheetColumnType.matchedSelect,
|
||||
value: field.key,
|
||||
matchedOptions,
|
||||
};
|
||||
@ -69,8 +67,8 @@ export const setColumn = <T extends string>(
|
||||
fieldOption.value === entry || fieldOption.label === entry,
|
||||
)?.value;
|
||||
return value
|
||||
? ({ entry, value } as MatchedOptions<T>)
|
||||
: ({ entry } as MatchedOptions<T>);
|
||||
? ({ entry, value } as SpreadsheetMatchedOptions<T>)
|
||||
: ({ entry } as SpreadsheetMatchedOptions<T>);
|
||||
});
|
||||
const areAllMatched =
|
||||
matchedOptions.filter((option) => option.value).length ===
|
||||
@ -79,8 +77,8 @@ export const setColumn = <T extends string>(
|
||||
return {
|
||||
...oldColumn,
|
||||
type: areAllMatched
|
||||
? ColumnType.matchedSelectOptions
|
||||
: ColumnType.matchedSelect,
|
||||
? SpreadsheetColumnType.matchedSelectOptions
|
||||
: SpreadsheetColumnType.matchedSelect,
|
||||
value: field.key,
|
||||
matchedOptions,
|
||||
};
|
||||
@ -89,7 +87,7 @@ export const setColumn = <T extends string>(
|
||||
if (field?.fieldType.type === 'checkbox') {
|
||||
return {
|
||||
index: oldColumn.index,
|
||||
type: ColumnType.matchedCheckbox,
|
||||
type: SpreadsheetColumnType.matchedCheckbox,
|
||||
value: field.key,
|
||||
header: oldColumn.header,
|
||||
};
|
||||
@ -98,7 +96,7 @@ export const setColumn = <T extends string>(
|
||||
if (field?.fieldType.type === 'input') {
|
||||
return {
|
||||
index: oldColumn.index,
|
||||
type: ColumnType.matched,
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: field.key,
|
||||
header: oldColumn.header,
|
||||
};
|
||||
@ -107,6 +105,6 @@ export const setColumn = <T extends string>(
|
||||
return {
|
||||
index: oldColumn.index,
|
||||
header: oldColumn.header,
|
||||
type: ColumnType.empty,
|
||||
type: SpreadsheetColumnType.empty,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import {
|
||||
Column,
|
||||
ColumnType,
|
||||
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
||||
|
||||
export const setIgnoreColumn = <T extends string>({
|
||||
header,
|
||||
index,
|
||||
}: Column<T>): Column<T> => ({
|
||||
}: SpreadsheetColumn<T>): SpreadsheetColumn<T> => ({
|
||||
header,
|
||||
index,
|
||||
type: ColumnType.ignored,
|
||||
type: SpreadsheetColumnType.ignored,
|
||||
});
|
||||
|
||||
@ -1,30 +1,34 @@
|
||||
import {
|
||||
ColumnType,
|
||||
MatchedOptions,
|
||||
MatchedSelectColumn,
|
||||
MatchedSelectOptionsColumn,
|
||||
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
SpreadsheetMatchedSelectColumn,
|
||||
SpreadsheetMatchedSelectOptionsColumn,
|
||||
} from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
||||
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
|
||||
|
||||
export const setSubColumn = <T>(
|
||||
oldColumn: MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T>,
|
||||
oldColumn:
|
||||
| SpreadsheetMatchedSelectColumn<T>
|
||||
| SpreadsheetMatchedSelectOptionsColumn<T>,
|
||||
entry: string,
|
||||
value: string,
|
||||
): MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T> => {
|
||||
):
|
||||
| SpreadsheetMatchedSelectColumn<T>
|
||||
| SpreadsheetMatchedSelectOptionsColumn<T> => {
|
||||
const options = oldColumn.matchedOptions.map((option) =>
|
||||
option.entry === entry ? { ...option, value } : option,
|
||||
);
|
||||
const allMathced = options.every(({ value }) => !!value);
|
||||
if (allMathced) {
|
||||
const allMatched = options.every(({ value }) => !!value);
|
||||
if (allMatched) {
|
||||
return {
|
||||
...oldColumn,
|
||||
matchedOptions: options as MatchedOptions<T>[],
|
||||
type: ColumnType.matchedSelectOptions,
|
||||
matchedOptions: options as SpreadsheetMatchedOptions<T>[],
|
||||
type: SpreadsheetColumnType.matchedSelectOptions,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...oldColumn,
|
||||
matchedOptions: options as MatchedOptions<T>[],
|
||||
type: ColumnType.matchedSelect,
|
||||
matchedOptions: options as SpreadsheetMatchedOptions<T>[],
|
||||
type: SpreadsheetColumnType.matchedSelect,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
import uniqBy from 'lodash.uniqby';
|
||||
|
||||
import {
|
||||
MatchColumnsStepProps,
|
||||
MatchedOptions,
|
||||
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { MatchColumnsStepProps } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
|
||||
|
||||
export const uniqueEntries = <T extends string>(
|
||||
data: MatchColumnsStepProps['data'],
|
||||
index: number,
|
||||
): Partial<MatchedOptions<T>>[] =>
|
||||
): Partial<SpreadsheetMatchedOptions<T>>[] =>
|
||||
uniqBy(
|
||||
data.map((row) => ({ entry: row[index] })),
|
||||
'entry',
|
||||
|
||||
@ -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 { SelectOption } from '@/spreadsheet-import/types';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const spacing1 = THEME_COMMON.spacing(1);
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -40,7 +38,7 @@ export const MultiSelectDisplay = ({
|
||||
key={index}
|
||||
color={selectedOption.color ?? 'transparent'}
|
||||
text={selectedOption.label}
|
||||
Icon={selectedOption.icon ?? undefined}
|
||||
Icon={selectedOption.Icon ?? undefined}
|
||||
/>
|
||||
))}
|
||||
</StyledContainer>
|
||||
|
||||
@ -3,7 +3,6 @@ import { useRecoilValue } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { SelectOption } from '@/spreadsheet-import/types';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
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 { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
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 { MenuItemMultiSelectTag, SelectOption } from 'twenty-ui';
|
||||
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
|
||||
|
||||
type MultiSelectInputProps = {
|
||||
selectableListComponentInstanceId: string;
|
||||
@ -128,7 +127,7 @@ export const MultiSelectInput = ({
|
||||
selected={values?.includes(option.value) || false}
|
||||
text={option.label}
|
||||
color={option.color ?? 'transparent'}
|
||||
Icon={option.icon ?? undefined}
|
||||
Icon={option.Icon ?? undefined}
|
||||
onClick={() =>
|
||||
onOptionSelected(formatNewSelectedOptions(option.value))
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { SelectOption } from '@/spreadsheet-import/types';
|
||||
import { SelectInput as SelectBaseInput } from '@/ui/input/components/SelectInput';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectOption } from 'twenty-ui';
|
||||
|
||||
type SelectInputProps = {
|
||||
selectableListComponentInstanceId: string;
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import styled from '@emotion/styled';
|
||||
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 { 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 { SelectControl } from '@/ui/input/components/SelectControl';
|
||||
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export type SelectOption<Value extends string | number | boolean | null> = {
|
||||
value: Value;
|
||||
label: string;
|
||||
Icon?: IconComponent;
|
||||
};
|
||||
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
|
||||
|
||||
export type SelectSizeVariant = 'small' | 'default';
|
||||
|
||||
|
||||
@ -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 styled from '@emotion/styled';
|
||||
import { IconChevronDown, OverflowingTextWithTooltip } from 'twenty-ui';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconChevronDown,
|
||||
OverflowingTextWithTooltip,
|
||||
SelectOption,
|
||||
} from 'twenty-ui';
|
||||
|
||||
const StyledControlContainer = styled.div<{
|
||||
disabled?: boolean;
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import { SelectOption } from '@/spreadsheet-import/types';
|
||||
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
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 { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { MenuItemSelectTag, TagColor } from 'twenty-ui';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { MenuItemSelectTag, SelectOption, TagColor } from 'twenty-ui';
|
||||
|
||||
interface SelectInputProps {
|
||||
onOptionSelected: (selectedOption: SelectOption) => void;
|
||||
@ -125,7 +123,7 @@ export const SelectInput = ({
|
||||
text={option.label}
|
||||
color={(option.color as TagColor) ?? 'transparent'}
|
||||
onClick={() => handleOptionChange(option)}
|
||||
LeftIcon={option.icon}
|
||||
LeftIcon={option.Icon}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
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 { 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 = ({
|
||||
label,
|
||||
|
||||
@ -2,7 +2,7 @@ import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilte
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
||||
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 { WorkflowCreateRecordAction } from '@/workflow/types/Workflow';
|
||||
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 { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
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 { useDebouncedCallback } from 'use-debounce';
|
||||
import { FieldMetadataType } from '~/generated/graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type WorkflowEditActionCreateRecordProps = {
|
||||
action: WorkflowCreateRecordAction;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
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 { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
|
||||
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 { useDebouncedCallback } from 'use-debounce';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type WorkflowEditActionDeleteRecordProps = {
|
||||
action: WorkflowDeleteRecordAction;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
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 { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
|
||||
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 { HorizontalSeparator, SelectOption, useIcons } from 'twenty-ui';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
type WorkflowEditActionFindRecordsProps = {
|
||||
action: WorkflowFindRecordsAction;
|
||||
|
||||
@ -7,7 +7,7 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
||||
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
|
||||
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 { WorkflowSendEmailAction } from '@/workflow/types/Workflow';
|
||||
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 { useEffect, useState } from 'react';
|
||||
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 { useDebouncedCallback } from 'use-debounce';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
|
||||
type WorkflowEditActionSendEmailProps = {
|
||||
action: WorkflowSendEmailAction;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { 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 { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||
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 { useDebouncedCallback } from 'use-debounce';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type WorkflowEditActionUpdateRecordProps = {
|
||||
action: WorkflowUpdateRecordAction;
|
||||
|
||||
@ -8,6 +8,7 @@ import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import camelCase from 'lodash.camelcase';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
IconSettingsAutomation,
|
||||
IconX,
|
||||
@ -15,7 +16,6 @@ import {
|
||||
IllustrationIconText,
|
||||
LightIconButton,
|
||||
} from 'twenty-ui';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
type WorkflowEditActionFormFieldSettingsProps = {
|
||||
field: WorkflowFormActionField;
|
||||
@ -109,13 +109,13 @@ export const WorkflowEditActionFormFieldSettings = ({
|
||||
label: getDefaultFormFieldSettings(FieldMetadataType.TEXT)
|
||||
.label,
|
||||
value: FieldMetadataType.TEXT,
|
||||
icon: IllustrationIconText,
|
||||
Icon: IllustrationIconText,
|
||||
},
|
||||
{
|
||||
label: getDefaultFormFieldSettings(FieldMetadataType.NUMBER)
|
||||
.label,
|
||||
value: FieldMetadataType.NUMBER,
|
||||
icon: IllustrationIconNumbers,
|
||||
Icon: IllustrationIconNumbers,
|
||||
},
|
||||
]}
|
||||
onChange={(newType: string | null) => {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
|
||||
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 { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useIcons } from 'twenty-ui';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { SelectOption, useIcons } from 'twenty-ui';
|
||||
|
||||
type WorkflowEditTriggerDatabaseEventFormProps = {
|
||||
trigger: WorkflowDatabaseEventTrigger;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import {
|
||||
WorkflowManualTrigger,
|
||||
WorkflowManualTriggerAvailability,
|
||||
@ -10,8 +10,8 @@ import { MANUAL_TRIGGER_AVAILABILITY_OPTIONS } from '@/workflow/workflow-trigger
|
||||
import { getManualTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettings';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useIcons } from 'twenty-ui';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { SelectOption, useIcons } from 'twenty-ui';
|
||||
|
||||
type WorkflowEditTriggerManualFormProps = {
|
||||
trigger: WorkflowManualTrigger;
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
IconRefresh,
|
||||
IconTrash,
|
||||
Section,
|
||||
SelectOption,
|
||||
useIcons,
|
||||
} from 'twenty-ui';
|
||||
|
||||
@ -19,14 +20,14 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { useWebhookUpdateForm } from '@/settings/developers/hooks/useWebhookUpdateForm';
|
||||
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 { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
|
||||
const OBJECT_DROPDOWN_WIDTH = 340;
|
||||
const ACTION_DROPDOWN_WIDTH = 140;
|
||||
|
||||
@ -31,6 +31,7 @@ const StyledTag = styled.h3<{
|
||||
const themeColor = theme.tag.background[color];
|
||||
|
||||
if (!isDefined(themeColor)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`Tag color ${color} is not defined in the theme`);
|
||||
return theme.tag.background.gray;
|
||||
} else {
|
||||
|
||||
@ -27,3 +27,4 @@ export * from './components/Radio';
|
||||
export * from './components/RadioGroup';
|
||||
export * from './components/Toggle';
|
||||
export * from './types/ColorScheme';
|
||||
export * from './types/SelectOption';
|
||||
|
||||
12
packages/twenty-ui/src/input/types/SelectOption.ts
Normal file
12
packages/twenty-ui/src/input/types/SelectOption.ts
Normal 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';
|
||||
};
|
||||
Reference in New Issue
Block a user