From e9e33c4d29db47301b208e1f53078d957ea4d957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Fri, 28 Mar 2025 07:56:51 +0100 Subject: [PATCH] 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` --- .../components/FormCountryCodeSelectInput.tsx | 3 +- .../components/FormCountrySelectInput.tsx | 3 +- .../components/FormMultiSelectFieldInput.tsx | 3 +- .../components/FormSelectFieldInput.tsx | 7 +- .../input/components/SelectFieldInput.tsx | 3 +- .../hooks/useBuildAvailableFieldsForImport.ts | 394 +++++++----------- .../types/AvailableFieldForImport.ts | 6 +- .../buildRecordFromImportedStructuredRow.ts | 2 +- ...etSpreadSheetFieldValidationDefinitions.ts | 6 +- .../AvailableTimezoneOptionsByLabel.ts | 4 +- .../SettingsDataModelFieldAddressForm.tsx | 13 +- ...SettingsDataModelObjectIdentifiersForm.tsx | 4 +- .../__mocks__/mockRsiValues.ts | 20 +- .../components/MatchColumnSelect.tsx | 61 +-- .../MatchColumnsStep/MatchColumnsStep.tsx | 77 +--- .../components/ColumnGrid.tsx | 11 +- .../components/SubMatchingSelect.tsx | 23 +- .../components/TemplateColumn.tsx | 13 +- .../components/UnmatchColumn.tsx | 11 +- .../components/UserTableColumn.tsx | 4 +- .../states/initialComputedColumnsState.ts | 18 +- .../UploadStep/components/ExampleTable.tsx | 4 +- .../UploadStep/components/columns.tsx | 8 +- .../ValidationStep/ValidationStep.tsx | 21 +- .../ValidationStep/components/columns.tsx | 11 +- .../steps/components/ValidationStep/types.ts | 4 +- .../steps/types/SpreadsheetImportStep.ts | 4 +- .../types/SpreadsheetColumn.ts | 52 +++ .../types/SpreadsheetColumnType.ts | 8 + .../types/SpreadsheetColumns.ts | 3 + .../types/SpreadsheetImportDialogOptions.ts | 61 +++ .../types/SpreadsheetImportErrorLevel.ts | 1 + .../types/SpreadsheetImportField.ts | 25 ++ .../types/SpreadsheetImportFieldType.ts | 28 ++ ...eadsheetImportFieldValidationDefinition.ts | 43 ++ .../types/SpreadsheetImportFields.ts | 6 + ...SpreadsheetImportImportValidationResult.ts | 9 + .../types/SpreadsheetImportImportedRow.ts | 1 + .../SpreadsheetImportImportedStructuredRow.ts | 3 + .../types/SpreadsheetImportInfo.ts | 6 + .../types/SpreadsheetImportRowHook.ts | 8 + .../types/SpreadsheetImportTableHook.ts | 11 + .../types/SpreadsheetMatchedOptions.ts | 4 + .../modules/spreadsheet-import/types/index.ts | 222 ++-------- .../utils/__tests__/dataMutations.test.ts | 75 ++-- .../utils/__tests__/findMatch.test.ts | 10 +- .../findUnmatchedRequiredFields.test.ts | 37 +- .../__tests__/generateExampleRow.test.ts | 14 +- .../utils/__tests__/getFieldOptions.test.ts | 8 +- .../utils/__tests__/getMatchedColumns.test.ts | 73 ++-- .../__tests__/normalizeTableData.test.ts | 61 +-- .../utils/__tests__/setColumn.test.ts | 32 +- .../utils/__tests__/setIgnoreColumn.test.ts | 12 +- .../utils/__tests__/setSubColumn.test.ts | 18 +- .../spreadsheet-import/utils/dataMutations.ts | 22 +- .../spreadsheet-import/utils/findMatch.ts | 5 +- .../utils/findUnmatchedRequiredFields.ts | 8 +- .../utils/generateExampleRow.ts | 14 +- .../utils/getFieldOptions.ts | 4 +- .../utils/getMatchedColumns.ts | 25 +- .../utils/normalizeTableData.ts | 26 +- .../spreadsheet-import/utils/setColumn.ts | 42 +- .../utils/setIgnoreColumn.ts | 10 +- .../spreadsheet-import/utils/setSubColumn.ts | 30 +- .../spreadsheet-import/utils/uniqueEntries.ts | 8 +- .../display/components/MultiSelectDisplay.tsx | 6 +- .../input/components/MultiSelectInput.tsx | 7 +- .../ui/field/input/components/SelectInput.tsx | 2 +- .../modules/ui/input/components/Select.tsx | 15 +- .../ui/input/components/SelectControl.tsx | 8 +- .../ui/input/components/SelectInput.tsx | 6 +- .../country/components/CountrySelect.tsx | 4 +- .../WorkflowEditActionCreateRecord.tsx | 6 +- .../WorkflowEditActionDeleteRecord.tsx | 6 +- .../WorkflowEditActionFindRecords.tsx | 6 +- .../WorkflowEditActionSendEmail.tsx | 8 +- .../WorkflowEditActionUpdateRecord.tsx | 6 +- .../WorkflowEditActionFormFieldSettings.tsx | 6 +- .../WorkflowEditTriggerDatabaseEventForm.tsx | 4 +- .../WorkflowEditTriggerManualForm.tsx | 4 +- .../SettingsDevelopersWebhookDetail.tsx | 5 +- .../src/display/tag/components/Tag.tsx | 1 + packages/twenty-ui/src/input/index.ts | 1 + .../twenty-ui/src/input/types/SelectOption.ts | 12 + 84 files changed, 960 insertions(+), 916 deletions(-) create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumn.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumnType.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumns.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportDialogOptions.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportErrorLevel.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportField.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFieldType.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFieldValidationDefinition.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFields.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportValidationResult.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportedRow.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportedStructuredRow.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportInfo.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportRowHook.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportTableHook.ts create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetMatchedOptions.ts create mode 100644 packages/twenty-ui/src/input/types/SelectOption.ts diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountryCodeSelectInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountryCodeSelectInput.tsx index 0728db47d..7f9d83e97 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountryCodeSelectInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountryCodeSelectInput.tsx @@ -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'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountrySelectInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountrySelectInput.tsx index 617a00db0..58b1a19ef 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountrySelectInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountrySelectInput.tsx @@ -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 = ({ diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormMultiSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormMultiSelectFieldInput.tsx index cf6471b7c..6b718f206 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormMultiSelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormMultiSelectFieldInput.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSelectFieldInput.tsx index 3858e9a2c..dde905075 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSelectFieldInput.tsx @@ -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 = ({ @@ -269,7 +268,7 @@ export const FormSelectFieldInput = ({ diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx index b682af500..f090c42c7 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts index 88a60f67f..27fb7dc9c 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts @@ -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 = {}, + 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; diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/types/AvailableFieldForImport.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/types/AvailableFieldForImport.ts index c15eab772..693dac58c 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/types/AvailableFieldForImport.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/types/AvailableFieldForImport.ts @@ -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; }; diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts index 1d67b2df6..cbacd4581 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions.ts index 86143a06d..069e694e3 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions.ts @@ -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 [ diff --git a/packages/twenty-front/src/modules/settings/accounts/constants/AvailableTimezoneOptionsByLabel.ts b/packages/twenty-front/src/modules/settings/accounts/constants/AvailableTimezoneOptionsByLabel.ts index 0b78523b4..6e8ea2b3a 100644 --- a/packages/twenty-front/src/modules/settings/accounts/constants/AvailableTimezoneOptionsByLabel.ts +++ b/packages/twenty-front/src/modules/settings/accounts/constants/AvailableTimezoneOptionsByLabel.ts @@ -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> + Record >((result, ianaTimeZone) => { const timeZoneLabel = formatTimeZoneLabel(ianaTimeZone); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm.tsx index 34ab262ec..91348b9fd 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm.tsx @@ -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>(({ countryName, Flag }) => ({ + .map(({ countryName, Flag }) => ({ label: countryName, value: countryName, Icon: (props: IconComponentProps) => diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx index 1a176d5d5..21ec8ec25 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx @@ -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'; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/__mocks__/mockRsiValues.ts b/packages/twenty-front/src/modules/spreadsheet-import/__mocks__/mockRsiValues.ts index 80e6d963e..d38985c40 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/__mocks__/mockRsiValues.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/__mocks__/mockRsiValues.ts @@ -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; +] as SpreadsheetImportFields; -export const importedColums: Columns = [ +export const importedColums: SpreadsheetColumns = [ { header: 'Name', index: 0, diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx index 871471498..84f4954b7 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx @@ -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 = ({ <>
- {options?.map((option) => ( - - handleChange(option)} - disabled={ - option.disabled && value?.value !== option.value - } - LeftIcon={option?.icon} - text={option.label} - /> - {option.disabled && - value?.value !== option.value && - createPortal( - , - document.body, - )} - - ))} + {options?.map((option) => { + const id = `${uuidV4()}-${option.value}`; + return ( + +
+ handleChange(option)} + disabled={ + option.disabled && value?.value !== option.value + } + LeftIcon={option?.Icon} + text={option.label} + /> +
+ {option.disabled && + value?.value !== option.value && + createPortal( + , + document.body, + )} +
+ ); + })} {options?.length === 0 && ( )} diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx index 84ae9ae78..d172bcbed 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx @@ -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 = { - entry: string; - value?: T; -}; - -type EmptyColumn = { type: ColumnType.empty; index: number; header: string }; - -type IgnoredColumn = { - type: ColumnType.ignored; - index: number; - header: string; -}; - -type MatchedColumn = { - type: ColumnType.matched; - index: number; - header: string; - value: T; -}; - -type MatchedSwitchColumn = { - type: ColumnType.matchedCheckbox; - index: number; - header: string; - value: T; -}; - -export type MatchedSelectColumn = { - type: ColumnType.matchedSelect; - index: number; - header: string; - value: T; - matchedOptions: Partial>[]; -}; - -export type MatchedSelectOptionsColumn = { - type: ColumnType.matchedSelectOptions; - index: number; - header: string; - value: T; - matchedOptions: MatchedOptions[]; -}; - -export type Column = - | EmptyColumn - | IgnoredColumn - | MatchedColumn - | MatchedSwitchColumn - | MatchedSelectColumn - | MatchedSelectOptionsColumn; - -export type Columns = Column[]; - export const MatchColumnsStep = ({ data, headerValues, @@ -179,7 +120,7 @@ export const MatchColumnsStep = ({ 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 = ({ } else { const field = fields.find( (field) => field.key === value, - ) as unknown as Field; + ) as unknown as SpreadsheetImportField; const existingFieldIndex = columns.findIndex( (column) => 'value' in column && column.value === field.key, ); setColumns( - columns.map>((column, index) => { + columns.map>((column, index) => { if (columnIndex === index) { return setColumn(column, field, data); } else if (index === existingFieldIndex) { @@ -223,7 +164,7 @@ export const MatchColumnsStep = ({ async ( values: ImportedStructuredRow[], rawData: ImportedRow[], - columns: Columns, + columns: SpreadsheetColumns, ) => { try { const data = await matchColumnsStepHook(values, rawData, columns); @@ -322,7 +263,7 @@ export const MatchColumnsStep = ({ useEffect(() => { const isInitialColumnsState = columns.every( - (column) => column.type === ColumnType.empty, + (column) => column.type === SpreadsheetColumnType.empty, ); if (autoMapHeaders && isInitialColumnsState) { setColumns(getMatchedColumns(columns, fields, data, autoMapDistance)); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/ColumnGrid.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/ColumnGrid.tsx index 3d1ad999a..2ca5b46e9 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/ColumnGrid.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/ColumnGrid.tsx @@ -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` `; type ColumnGridProps = { - columns: Columns; + columns: SpreadsheetColumns; renderUserColumn: ( - columns: Columns, + columns: SpreadsheetColumns, columnIndex: number, ) => React.ReactNode; renderTemplateColumn: ( - columns: Columns, + columns: SpreadsheetColumns, columnIndex: number, ) => React.ReactNode; renderUnmatchedColumn: ( - columns: Columns, + columns: SpreadsheetColumns, columnIndex: number, ) => React.ReactNode; }; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelect.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelect.tsx index cdce6b1e1..6c1b80132 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelect.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelect.tsx @@ -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 { - option: MatchedOptions | Partial>; - column: MatchedSelectColumn | MatchedSelectOptionsColumn; + option: SpreadsheetMatchedOptions | Partial>; + column: + | SpreadsheetMatchedSelectColumn + | SpreadsheetMatchedSelectOptionsColumn; onSubChange: (val: T, index: number, option: string) => void; placeholder: string; - selectedOption?: MatchedOptions | Partial>; + selectedOption?: + | SpreadsheetMatchedOptions + | Partial>; } export const SubMatchingSelect = ({ diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/TemplateColumn.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/TemplateColumn.tsx index 5ae4c33d2..34833ac12 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/TemplateColumn.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/TemplateColumn.tsx @@ -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 = { - columns: Columns; + columns: SpreadsheetColumns; columnIndex: number; onChange: (val: T, index: number) => void; }; @@ -27,13 +28,13 @@ export const TemplateColumn = ({ }: TemplateColumnProps) => { const { fields } = useSpreadsheetImportInternal(); 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 = ({ }) !== -1; return { - icon: icon, + Icon: Icon, value: key, label: label, disabled: isSelected, @@ -52,7 +53,7 @@ export const TemplateColumn = ({ const selectOptions = [ { - icon: IconForbid, + Icon: IconForbid, value: 'do-not-import', label: t`Do not import`, }, diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumn.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumn.tsx index 7fe11abba..631a803b1 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumn.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumn.tsx @@ -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 = ( - fields: Fields, - column: Column, + fields: SpreadsheetImportFields, + column: SpreadsheetColumn, ) => { const fieldLabel = fields.find( (field) => 'value' in column && field.key === column.value, @@ -24,7 +25,7 @@ const getExpandableContainerTitle = ( }; type UnmatchColumnProps = { - columns: Column[]; + columns: SpreadsheetColumns; columnIndex: number; onSubChange: (val: T, index: number, option: string) => void; }; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/UserTableColumn.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/UserTableColumn.tsx index 1330472aa..36ecc6388 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/UserTableColumn.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/UserTableColumn.tsx @@ -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 = { - column: Column; + column: SpreadsheetColumn; importedRow: ImportedRow; }; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/states/initialComputedColumnsState.ts b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/states/initialComputedColumnsState.ts index 9b41943cd..0698e795b 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/states/initialComputedColumnsState.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/states/initialComputedColumnsState.ts @@ -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, + default: [] as SpreadsheetColumns, }); export const initialComputedColumnsSelector = selectorFamily< - Columns, + SpreadsheetColumns, ImportedRow >({ key: 'initialComputedColumnsSelector', get: (headerValues: ImportedRow) => ({ get }) => { - const currentState = get(matchColumnsState) as Columns; + const currentState = get(matchColumnsState) as SpreadsheetColumns; 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; + return initialState as SpreadsheetColumns; } else { return currentState; } @@ -36,6 +34,6 @@ export const initialComputedColumnsSelector = selectorFamily< set: () => ({ set }, newValue) => { - set(matchColumnsState, newValue as Columns); + set(matchColumnsState, newValue as SpreadsheetColumns); }, }); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/ExampleTable.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/ExampleTable.tsx index 562b3c478..9001a1017 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/ExampleTable.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/ExampleTable.tsx @@ -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 { - fields: Fields; + fields: SpreadsheetImportFields; } export const ExampleTable = ({ diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/columns.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/columns.tsx index 6ec617b1e..d15195c1d 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/columns.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/columns.tsx @@ -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 = (fields: Fields) => +export const generateColumns = ( + fields: SpreadsheetImportFields, +) => fields.map( (column): Column => ({ key: column.key, diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx index ca1c52872..2b9869b5b 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx @@ -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 = { initialData: ImportedStructuredRow[]; - importedColumns: Columns; + importedColumns: SpreadsheetColumns; file: File; onBack: () => void; setCurrentStepState: Dispatch>; @@ -153,13 +151,14 @@ export const ValidationStep = ({ 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 = ({ validStructuredRows: [] as ImportedStructuredRow[], invalidStructuredRows: [] as ImportedStructuredRow[], allStructuredRows: data, - } satisfies ImportValidationResult, + } satisfies SpreadsheetImportImportValidationResult, ); setCurrentStepState({ diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx index 3b2cd9f72..b035eae1c 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx @@ -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 = ( - fields: Fields, + fields: SpreadsheetImportFields, ): Column & ImportedStructuredRowMetadata>[] => [ { key: SELECT_COLUMN_KEY, @@ -135,7 +138,7 @@ export const generateColumns = ( value={ value ? ({ - icon: undefined, + Icon: undefined, ...value, } as const) : value diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/types.ts b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/types.ts index bf72b163d..3c1073fe3 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/types.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/types.ts @@ -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 }; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/types/SpreadsheetImportStep.ts b/packages/twenty-front/src/modules/spreadsheet-import/steps/types/SpreadsheetImportStep.ts index ad04f1512..6e13bb902 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/types/SpreadsheetImportStep.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/types/SpreadsheetImportStep.ts @@ -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; + importedColumns: SpreadsheetColumns; } | { type: SpreadsheetImportStepType.loading; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumn.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumn.ts new file mode 100644 index 000000000..475bb65d3 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumn.ts @@ -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 = { + type: SpreadsheetColumnType.matched; + index: number; + header: string; + value: T; +}; + +type SpreadsheetMatchedSwitchColumn = { + type: SpreadsheetColumnType.matchedCheckbox; + index: number; + header: string; + value: T; +}; + +export type SpreadsheetMatchedSelectColumn = { + type: SpreadsheetColumnType.matchedSelect; + index: number; + header: string; + value: T; + matchedOptions: Partial>[]; +}; + +export type SpreadsheetMatchedSelectOptionsColumn = { + type: SpreadsheetColumnType.matchedSelectOptions; + index: number; + header: string; + value: T; + matchedOptions: SpreadsheetMatchedOptions[]; +}; + +export type SpreadsheetColumn = + | SpreadsheetEmptyColumn + | SpreadsheetIgnoredColumn + | SpreadsheetMatchedColumn + | SpreadsheetMatchedSwitchColumn + | SpreadsheetMatchedSelectColumn + | SpreadsheetMatchedSelectOptionsColumn; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumnType.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumnType.ts new file mode 100644 index 000000000..d7cb38871 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumnType.ts @@ -0,0 +1,8 @@ +export enum SpreadsheetColumnType { + empty, + ignored, + matched, + matchedCheckbox, + matchedSelect, + matchedSelectOptions, +} diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumns.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumns.ts new file mode 100644 index 000000000..e53cfb5e0 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetColumns.ts @@ -0,0 +1,3 @@ +import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn'; + +export type SpreadsheetColumns = SpreadsheetColumn[]; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportDialogOptions.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportDialogOptions.ts new file mode 100644 index 000000000..62230c8be --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportDialogOptions.ts @@ -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 = { + // Is modal visible. + isOpen: boolean; + // callback when RSI is closed before final submit + onClose: () => void; + // Field description for requested data + fields: SpreadsheetImportFields; + // Runs after file upload step, receives and returns raw sheet data + uploadStepHook?: (importedRows: ImportedRow[]) => Promise; + // 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[], + importedRows: ImportedRow[], + columns: SpreadsheetColumns, + ) => Promise[]>; + // Runs after column matching and on entry change + rowHook?: SpreadsheetImportRowHook; + // Runs after column matching and on entry change + tableHook?: SpreadsheetImportTableHook; + // Function called after user finishes the flow + onSubmit: ( + validationResult: SpreadsheetImportImportValidationResult, + file: File, + ) => Promise; + // 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; +}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportErrorLevel.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportErrorLevel.ts new file mode 100644 index 000000000..193e08ba1 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportErrorLevel.ts @@ -0,0 +1 @@ +export type SpreadsheetImportErrorLevel = 'info' | 'warning' | 'error'; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportField.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportField.ts new file mode 100644 index 000000000..8482f8300 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportField.ts @@ -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 = { + // 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; +}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFieldType.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFieldType.ts new file mode 100644 index 000000000..fbfb921e3 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFieldType.ts @@ -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; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFieldValidationDefinition.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFieldValidationDefinition.ts new file mode 100644 index 000000000..c5fa54d80 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFieldValidationDefinition.ts @@ -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; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFields.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFields.ts new file mode 100644 index 000000000..8a1549867 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportFields.ts @@ -0,0 +1,6 @@ +import { SpreadsheetImportField } from '@/spreadsheet-import/types/SpreadsheetImportField'; +import { ReadonlyDeep } from 'type-fest'; + +export type SpreadsheetImportFields = ReadonlyDeep< + SpreadsheetImportField[] +>; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportValidationResult.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportValidationResult.ts new file mode 100644 index 000000000..d2016772a --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportValidationResult.ts @@ -0,0 +1,9 @@ +import { ImportedStructuredRowMetadata } from '@/spreadsheet-import/steps/components/ValidationStep/types'; +import { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow'; + +export type SpreadsheetImportImportValidationResult = { + validStructuredRows: ImportedStructuredRow[]; + invalidStructuredRows: ImportedStructuredRow[]; + allStructuredRows: (ImportedStructuredRow & + ImportedStructuredRowMetadata)[]; +}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportedRow.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportedRow.ts new file mode 100644 index 000000000..d91f91d7c --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportedRow.ts @@ -0,0 +1 @@ +export type ImportedRow = Array; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportedStructuredRow.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportedStructuredRow.ts new file mode 100644 index 000000000..1c428117b --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportImportedStructuredRow.ts @@ -0,0 +1,3 @@ +export type ImportedStructuredRow = { + [key in T]: string | boolean | undefined; +}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportInfo.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportInfo.ts new file mode 100644 index 000000000..b5bdec959 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportInfo.ts @@ -0,0 +1,6 @@ +import { SpreadsheetImportErrorLevel } from './SpreadsheetImportErrorLevel'; + +export type SpreadsheetImportInfo = { + message: string; + level: SpreadsheetImportErrorLevel; +}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportRowHook.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportRowHook.ts new file mode 100644 index 000000000..a3db443fe --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportRowHook.ts @@ -0,0 +1,8 @@ +import { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow'; +import { SpreadsheetImportInfo } from './SpreadsheetImportInfo'; + +export type SpreadsheetImportRowHook = ( + row: ImportedStructuredRow, + addError: (fieldKey: T, error: SpreadsheetImportInfo) => void, + table: ImportedStructuredRow[], +) => ImportedStructuredRow; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportTableHook.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportTableHook.ts new file mode 100644 index 000000000..7459de943 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportTableHook.ts @@ -0,0 +1,11 @@ +import { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow'; +import { SpreadsheetImportInfo } from './SpreadsheetImportInfo'; + +export type SpreadsheetImportTableHook = ( + table: ImportedStructuredRow[], + addError: ( + rowIndex: number, + fieldKey: T, + error: SpreadsheetImportInfo, + ) => void, +) => ImportedStructuredRow[]; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetMatchedOptions.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetMatchedOptions.ts new file mode 100644 index 000000000..98beecf99 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetMatchedOptions.ts @@ -0,0 +1,4 @@ +export type SpreadsheetMatchedOptions = { + entry: string; + value?: T; +}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/index.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/index.ts index d41e8e60b..808e969d0 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/types/index.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/index.ts @@ -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 = { - // Is modal visible. - isOpen: boolean; - // callback when RSI is closed before final submit - onClose: () => void; - // Field description for requested data - fields: Fields; - // Runs after file upload step, receives and returns raw sheet data - uploadStepHook?: (importedRows: ImportedRow[]) => Promise; - // 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[], - importedRows: ImportedRow[], - columns: Columns, - ) => Promise[]>; - // Runs after column matching and on entry change - rowHook?: RowHook; - // Runs after column matching and on entry change - tableHook?: TableHook; - // Function called after user finishes the flow - onSubmit: ( - validationResult: ImportValidationResult, - file: File, - ) => Promise; - // 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; - -export type ImportedStructuredRow = { - [key in T]: string | boolean | undefined; -}; - -// Data model RSI uses for spreadsheet imports -export type Fields = ReadonlyDeep[]>; - -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 = { - // 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 = ( - row: ImportedStructuredRow, - addError: (fieldKey: T, error: Info) => void, - table: ImportedStructuredRow[], -) => ImportedStructuredRow; - -export type TableHook = ( - table: ImportedStructuredRow[], - addError: (rowIndex: number, fieldKey: T, error: Info) => void, -) => ImportedStructuredRow[]; - -export type ErrorLevel = 'info' | 'warning' | 'error'; - -export type Info = { - message: string; - level: ErrorLevel; -}; - -export type ImportValidationResult = { - validStructuredRows: ImportedStructuredRow[]; - invalidStructuredRows: ImportedStructuredRow[]; - allStructuredRows: (ImportedStructuredRow & - 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'; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/dataMutations.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/dataMutations.test.ts index ed922a910..5d62ad5bf 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/dataMutations.test.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/dataMutations.test.ts @@ -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( diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findMatch.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findMatch.test.ts index e48ae12e5..84c1c0cc4 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findMatch.test.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findMatch.test.ts @@ -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', diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findUnmatchedRequiredFields.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findUnmatchedRequiredFields.test.ts index b58274eba..f6c875249 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findUnmatchedRequiredFields.test.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/findUnmatchedRequiredFields.test.ts @@ -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 = { - type: ColumnType.matched, +const nameColumn: SpreadsheetColumn = { + type: SpreadsheetColumnType.matched, index: 0, header: '', value: 'Name', }; -const ageColumn: Column = { - type: ColumnType.matched, +const ageColumn: SpreadsheetColumn = { + type: SpreadsheetColumnType.matched, index: 0, header: '', value: 'Age', }; -const extraColumn: Column = { - type: ColumnType.matched, +const extraColumn: SpreadsheetColumn = { + type: SpreadsheetColumnType.matched, index: 0, header: '', value: 'Age', diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/generateExampleRow.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/generateExampleRow.test.ts index ab7285f6e..364ecddd0 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/generateExampleRow.test.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/generateExampleRow.test.ts @@ -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', diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getFieldOptions.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getFieldOptions.test.ts index 0e54a636c..8b770c964 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getFieldOptions.test.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getFieldOptions.test.ts @@ -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', diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getMatchedColumns.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getMatchedColumns.test.ts index 375ce5ff3..82b8e063d 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getMatchedColumns.test.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/getMatchedColumns.test.ts @@ -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[] = [ - { index: 0, header: 'Name', type: ColumnType.matched, value: 'Name' }, + const columns: SpreadsheetColumn[] = [ + { + 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[] = [ + const fields: SpreadsheetImportField[] = [ { 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[] = [ - { index: 0, header: 'Name', type: ColumnType.matched, value: 'Name' }, - { index: 1, header: 'Name', type: ColumnType.matched, value: 'Name' }, + const columnsWithDuplicates: SpreadsheetColumn[] = [ + { + 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[] = [ + const unmatchedFields: SpreadsheetImportField[] = [ { 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, }, ]; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/normalizeTableData.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/normalizeTableData.test.ts index 98f2511c2..527f7ca1c 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/normalizeTableData.test.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/normalizeTableData.test.ts @@ -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[] = [ - { index: 0, header: 'Name', type: ColumnType.matched, value: 'name' }, - { index: 1, header: 'Age', type: ColumnType.matched, value: 'age' }, + const columns: SpreadsheetColumn[] = [ + { + 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[] = [ + const fields: SpreadsheetImportField[] = [ { 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[] = [ + const columns: SpreadsheetColumn[] = [ { index: 0, header: 'Active', - type: ColumnType.matchedCheckbox, + type: SpreadsheetColumnType.matchedCheckbox, value: 'active', }, ]; - const fields: Field[] = [ + const fields: SpreadsheetImportField[] = [ { 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[] = [ + const columns: SpreadsheetColumn[] = [ { 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[] = [ + const fields: SpreadsheetImportField[] = [ { 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[] = [ - { index: 0, header: 'Empty', type: ColumnType.empty }, - { index: 1, header: 'Ignored', type: ColumnType.ignored }, + const columns: SpreadsheetColumn[] = [ + { 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[] = [ + const columns: SpreadsheetColumns = [ { index: 0, header: 'Unrecognized', - type: 'Unknown' as unknown as ColumnType.matched, + type: 'Unknown' as unknown as SpreadsheetColumnType.matched, value: '', }, ]; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setColumn.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setColumn.test.ts index 5c578cc80..0aae11bab 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setColumn.test.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setColumn.test.ts @@ -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, }); }); }); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setIgnoreColumn.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setIgnoreColumn.test.ts index ea6ed4a1d..edf3345f9 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setIgnoreColumn.test.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setIgnoreColumn.test.ts @@ -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, }); }); }); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setSubColumn.test.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setSubColumn.test.ts index 785c89d34..e764f4b6e 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setSubColumn.test.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/setSubColumn.test.ts @@ -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' }, diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/dataMutations.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/dataMutations.ts index b14d8fc54..c06cebfde 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/dataMutations.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/dataMutations.ts @@ -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 = ( data: (ImportedStructuredRow & Partial)[], - fields: Fields, - rowHook?: RowHook, - tableHook?: TableHook, + fields: SpreadsheetImportFields, + rowHook?: SpreadsheetImportRowHook, + tableHook?: SpreadsheetImportTableHook, ): (ImportedStructuredRow & 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, diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/findMatch.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/findMatch.ts index aa018a38d..6a6659697 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/findMatch.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/findMatch.ts @@ -1,7 +1,6 @@ +import { SpreadsheetImportFields } from '@/spreadsheet-import/types'; import lavenstein from 'js-levenshtein'; -import { Fields } from '@/spreadsheet-import/types'; - type AutoMatchAccumulator = { distance: number; value: T; @@ -9,7 +8,7 @@ type AutoMatchAccumulator = { export const findMatch = ( header: string, - fields: Fields, + fields: SpreadsheetImportFields, autoMapDistance: number, ): T | undefined => { const smallestValue = fields.reduce>((acc, field) => { diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts index 71af18402..f31908931 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts @@ -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 = ( - fields: Fields, - columns: Columns, + fields: SpreadsheetImportFields, + columns: SpreadsheetColumns, ) => fields .filter((field) => diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/generateExampleRow.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/generateExampleRow.ts index aee2c6469..493e7c447 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/generateExampleRow.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/generateExampleRow.ts @@ -1,13 +1,21 @@ -import { Field, Fields } from '@/spreadsheet-import/types'; +import { + SpreadsheetImportField, + SpreadsheetImportFields, +} from '@/spreadsheet-import/types'; -const titleMap: Record['fieldType']['type'], string> = { +const titleMap: Record< + SpreadsheetImportField['fieldType']['type'], + string +> = { checkbox: 'Boolean', select: 'Options', multiSelect: 'Options', input: 'Text', }; -export const generateExampleRow = (fields: Fields) => [ +export const generateExampleRow = ( + fields: SpreadsheetImportFields, +) => [ fields.reduce( (acc, field) => { acc[field.key as T] = field.example || titleMap[field.fieldType.type]; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/getFieldOptions.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/getFieldOptions.ts index 200953e6d..eb867c25f 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/getFieldOptions.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/getFieldOptions.ts @@ -1,7 +1,7 @@ -import { Fields } from '@/spreadsheet-import/types'; +import { SpreadsheetImportFields } from '@/spreadsheet-import/types'; export const getFieldOptions = ( - fields: Fields, + fields: SpreadsheetImportFields, fieldKey: string, ) => { const field = fields.find(({ key }) => fieldKey === key); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumns.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumns.ts index 3540b7aa0..b52c0ae1e 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumns.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumns.ts @@ -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 = ( - columns: Columns, - fields: Fields, + columns: SpreadsheetColumns, + fields: SpreadsheetImportFields, data: MatchColumnsStepProps['data'], autoMapDistance: number, ) => - columns.reduce[]>((arr, column) => { + columns.reduce[]>((arr, column) => { const autoMatch = findMatch(column.header, fields, autoMapDistance); if (isDefined(autoMatch)) { - const field = fields.find((field) => field.key === autoMatch) as Field; + const field = fields.find( + (field) => field.key === autoMatch, + ) as SpreadsheetImportField; const duplicateIndex = arr.findIndex( (column) => 'value' in column && column.value === field.key, ); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/normalizeTableData.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/normalizeTableData.ts index 247cdd0df..368fe71f3 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/normalizeTableData.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/normalizeTableData.ts @@ -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 = ( - columns: Columns, + columns: SpreadsheetColumns, data: ImportedRow[], - fields: Fields, + fields: SpreadsheetImportFields, ) => 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 = ( } 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 = ( } return acc; } - case ColumnType.empty: - case ColumnType.ignored: { + case SpreadsheetColumnType.empty: + case SpreadsheetColumnType.ignored: { return acc; } default: diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/setColumn.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/setColumn.ts index 784712fbe..d1d8100a0 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/setColumn.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/setColumn.ts @@ -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 = ( - oldColumn: Column, - field?: Field, + oldColumn: SpreadsheetColumn, + field?: SpreadsheetImportField, data?: MatchColumnsStepProps['data'], -): Column => { +): SpreadsheetColumn => { if (field?.fieldType.type === 'select') { const fieldOptions = field.fieldType.options; const uniqueData = uniqueEntries( data || [], oldColumn.index, - ) as MatchedOptions[]; + ) as SpreadsheetMatchedOptions[]; const matchedOptions = uniqueData.map((record) => { const value = fieldOptions.find( @@ -28,8 +26,8 @@ export const setColumn = ( fieldOption.label === record.entry, )?.value; return value - ? ({ ...record, value } as MatchedOptions) - : (record as MatchedOptions); + ? ({ ...record, value } as SpreadsheetMatchedOptions) + : (record as SpreadsheetMatchedOptions); }); const allMatched = matchedOptions.filter((o) => o.value).length === uniqueData?.length; @@ -37,8 +35,8 @@ export const setColumn = ( return { ...oldColumn, type: allMatched - ? ColumnType.matchedSelectOptions - : ColumnType.matchedSelect, + ? SpreadsheetColumnType.matchedSelectOptions + : SpreadsheetColumnType.matchedSelect, value: field.key, matchedOptions, }; @@ -69,8 +67,8 @@ export const setColumn = ( fieldOption.value === entry || fieldOption.label === entry, )?.value; return value - ? ({ entry, value } as MatchedOptions) - : ({ entry } as MatchedOptions); + ? ({ entry, value } as SpreadsheetMatchedOptions) + : ({ entry } as SpreadsheetMatchedOptions); }); const areAllMatched = matchedOptions.filter((option) => option.value).length === @@ -79,8 +77,8 @@ export const setColumn = ( return { ...oldColumn, type: areAllMatched - ? ColumnType.matchedSelectOptions - : ColumnType.matchedSelect, + ? SpreadsheetColumnType.matchedSelectOptions + : SpreadsheetColumnType.matchedSelect, value: field.key, matchedOptions, }; @@ -89,7 +87,7 @@ export const setColumn = ( 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 = ( 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 = ( return { index: oldColumn.index, header: oldColumn.header, - type: ColumnType.empty, + type: SpreadsheetColumnType.empty, }; }; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/setIgnoreColumn.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/setIgnoreColumn.ts index ae10ee398..7123369cd 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/setIgnoreColumn.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/setIgnoreColumn.ts @@ -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 = ({ header, index, -}: Column): Column => ({ +}: SpreadsheetColumn): SpreadsheetColumn => ({ header, index, - type: ColumnType.ignored, + type: SpreadsheetColumnType.ignored, }); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/setSubColumn.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/setSubColumn.ts index cf92beb6a..033096043 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/setSubColumn.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/setSubColumn.ts @@ -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 = ( - oldColumn: MatchedSelectColumn | MatchedSelectOptionsColumn, + oldColumn: + | SpreadsheetMatchedSelectColumn + | SpreadsheetMatchedSelectOptionsColumn, entry: string, value: string, -): MatchedSelectColumn | MatchedSelectOptionsColumn => { +): + | SpreadsheetMatchedSelectColumn + | SpreadsheetMatchedSelectOptionsColumn => { 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[], - type: ColumnType.matchedSelectOptions, + matchedOptions: options as SpreadsheetMatchedOptions[], + type: SpreadsheetColumnType.matchedSelectOptions, }; } else { return { ...oldColumn, - matchedOptions: options as MatchedOptions[], - type: ColumnType.matchedSelect, + matchedOptions: options as SpreadsheetMatchedOptions[], + type: SpreadsheetColumnType.matchedSelect, }; } }; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/uniqueEntries.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/uniqueEntries.ts index 803f37c5a..16d825de4 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/uniqueEntries.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/uniqueEntries.ts @@ -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 = ( data: MatchColumnsStepProps['data'], index: number, -): Partial>[] => +): Partial>[] => uniqBy( data.map((row) => ({ entry: row[index] })), 'entry', diff --git a/packages/twenty-front/src/modules/ui/field/display/components/MultiSelectDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/MultiSelectDisplay.tsx index aa117bd46..a3d8091d8 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/MultiSelectDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/MultiSelectDisplay.tsx @@ -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} /> ))} diff --git a/packages/twenty-front/src/modules/ui/field/input/components/MultiSelectInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/MultiSelectInput.tsx index a231f89ae..5c65ffea2 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/MultiSelectInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/MultiSelectInput.tsx @@ -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)) } diff --git a/packages/twenty-front/src/modules/ui/field/input/components/SelectInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/SelectInput.tsx index 579f4817a..aa3469631 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/SelectInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/SelectInput.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/ui/input/components/Select.tsx b/packages/twenty-front/src/modules/ui/input/components/Select.tsx index 1577b8fd8..1ba6270f4 100644 --- a/packages/twenty-front/src/modules/ui/input/components/Select.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/Select.tsx @@ -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: Value; - label: string; - Icon?: IconComponent; -}; +import { SelectHotkeyScope } from '../types/SelectHotkeyScope'; export type SelectSizeVariant = 'small' | 'default'; diff --git a/packages/twenty-front/src/modules/ui/input/components/SelectControl.tsx b/packages/twenty-front/src/modules/ui/input/components/SelectControl.tsx index 04e6e3366..b1b5464b1 100644 --- a/packages/twenty-front/src/modules/ui/input/components/SelectControl.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/SelectControl.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/ui/input/components/SelectInput.tsx b/packages/twenty-front/src/modules/ui/input/components/SelectInput.tsx index 4d90f9e7b..0a1b2cb41 100644 --- a/packages/twenty-front/src/modules/ui/input/components/SelectInput.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/SelectInput.tsx @@ -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} /> ); })} diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/country/components/CountrySelect.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/country/components/CountrySelect.tsx index 9d18f2129..d483e623b 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/country/components/CountrySelect.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/country/components/CountrySelect.tsx @@ -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, diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord.tsx index 4c2fc1f46..bb1713632 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord.tsx index c24d42756..e532bbcb2 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFindRecords.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFindRecords.tsx index 60bbe3069..13687ebeb 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFindRecords.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFindRecords.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail.tsx index bbbb1ba20..24b051de0 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx index 707a9b710..4cf9414fc 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFieldSettings.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFieldSettings.tsx index 12c184c61..c53b38231 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFieldSettings.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFieldSettings.tsx @@ -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) => { diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm.tsx b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm.tsx index 4cdac68cd..68415b846 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm.tsx b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm.tsx index 80f62dca4..4ecc8910a 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm.tsx @@ -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; diff --git a/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx b/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx index 6ab89002b..8d885d4c8 100644 --- a/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx +++ b/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx @@ -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; diff --git a/packages/twenty-ui/src/display/tag/components/Tag.tsx b/packages/twenty-ui/src/display/tag/components/Tag.tsx index ea14701da..5b1879118 100644 --- a/packages/twenty-ui/src/display/tag/components/Tag.tsx +++ b/packages/twenty-ui/src/display/tag/components/Tag.tsx @@ -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 { diff --git a/packages/twenty-ui/src/input/index.ts b/packages/twenty-ui/src/input/index.ts index f895f4273..c2df3cb9d 100644 --- a/packages/twenty-ui/src/input/index.ts +++ b/packages/twenty-ui/src/input/index.ts @@ -27,3 +27,4 @@ export * from './components/Radio'; export * from './components/RadioGroup'; export * from './components/Toggle'; export * from './types/ColorScheme'; +export * from './types/SelectOption'; diff --git a/packages/twenty-ui/src/input/types/SelectOption.ts b/packages/twenty-ui/src/input/types/SelectOption.ts new file mode 100644 index 000000000..369b1cace --- /dev/null +++ b/packages/twenty-ui/src/input/types/SelectOption.ts @@ -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'; +};