From bf704bd1bc3221928a97ba69df05f8eb98e0b105 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Wed, 16 Apr 2025 15:08:24 +0200 Subject: [PATCH] Improve CSV import sub-field selection (#11601) This PR adds a better UX for selecting sub-fields when importing CSV files. Before : image After : image image - A util `getSubFieldOptionKey` has been made to be able to reference the sub field in the `options` object of the spreadsheet import. - New components have been created : `MatchColumnSelectFieldSelectDropdownContent`, `MatchColumnSelectSubFieldSelectDropdownContent` and `MatchColumnSelectV2` - Extracted the hard-coded option do not import into a constant `DO_NOT_IMPORT_OPTION_KEY` - Passed `availableFieldMetadataItems` to spreadsheet global options so it's available anywhere in the hierarchy of components. --------- Co-authored-by: Charles Bochet Co-authored-by: Charles Bochet --- .../constants/CompositeFieldImportLabels.ts | 3 +- .../hooks/useBuildAvailableFieldsForImport.ts | 16 +- ...penObjectRecordsSpreadsheetImportDialog.ts | 1 + .../utils/getSubFieldOptionKey.ts | 17 +++ .../__mocks__/mockRsiValues.ts | 1 + .../components/MatchColumnSelect.tsx | 138 ----------------- ...ColumnSelectFieldSelectDropdownContent.tsx | 103 +++++++++++++ ...umnSelectSubFieldSelectDropdownContent.tsx | 109 +++++++++++++ .../components/MatchColumnToFieldSelect.tsx | 144 ++++++++++++++++++ .../components/ModalCloseButton.tsx | 2 +- .../constants/DoNotImportOptionKey.ts | 1 + .../__tests__/useSpreadsheetImport.test.tsx | 1 + .../MatchColumnsStep/MatchColumnsStep.tsx | 3 +- .../components/SubMatchingSelect.tsx | 130 ---------------- .../components/TemplateColumn.tsx | 9 +- .../ValidationStep/components/columns.tsx | 29 +--- .../types/SpreadsheetImportDialogOptions.ts | 4 +- 17 files changed, 409 insertions(+), 302 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSubFieldOptionKey.ts delete mode 100644 packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent.tsx create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/constants/DoNotImportOptionKey.ts delete mode 100644 packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelect.tsx diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts index 1b336a5e3..41d19c33b 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts @@ -1,4 +1,5 @@ import { + FieldActorValue, FieldAddressValue, FieldCurrencyValue, FieldEmailsValue, @@ -46,5 +47,5 @@ export const COMPOSITE_FIELD_IMPORT_LABELS = { } satisfies Partial>, [FieldMetadataType.ACTOR]: { sourceLabel: 'Source', - }, + } satisfies Partial>, }; 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 2bf19f867..2eefded44 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 @@ -3,8 +3,8 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels'; import { AvailableFieldForImport } from '@/object-record/spreadsheet-import/types/AvailableFieldForImport'; import { getSpreadSheetFieldValidationDefinitions } from '@/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; import { useIcons } from 'twenty-ui/display'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; type CompositeFieldType = keyof typeof COMPOSITE_FIELD_IMPORT_LABELS; @@ -42,17 +42,17 @@ export const useBuildAvailableFieldsForImport = () => { validationTypeResolver?: ValidationTypeResolver, ) => { Object.entries(COMPOSITE_FIELD_IMPORT_LABELS[fieldType]).forEach( - ([key, fieldLabel]) => { - const label = `${fieldLabel} (${fieldMetadataItem.label})`; + ([key, subFieldLabel]) => { + const label = `${fieldMetadataItem.label} / ${subFieldLabel}`; // Use the custom validation type if provided, otherwise use the field's type const validationType = validationTypeResolver - ? validationTypeResolver(key, fieldLabel) + ? validationTypeResolver(key, subFieldLabel) : fieldMetadataItem.type; availableFieldsForImport.push( createBaseField(fieldMetadataItem, { label, - key: `${fieldLabel} (${fieldMetadataItem.name})`, + key: `${subFieldLabel} (${fieldMetadataItem.name})`, fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(validationType, label), }), @@ -137,6 +137,12 @@ export const useBuildAvailableFieldsForImport = () => { currencyValidationResolver, ); }, + [FieldMetadataType.ACTOR]: (fieldMetadataItem) => { + handleCompositeFieldWithLabels( + fieldMetadataItem, + FieldMetadataType.ACTOR, + ); + }, [FieldMetadataType.RELATION]: (fieldMetadataItem) => { const label = `${fieldMetadataItem.label} (ID)`; availableFieldsForImport.push( diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts index 3c6647433..deff8a0ba 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts @@ -74,6 +74,7 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = ( } }, fields: availableFields, + availableFieldMetadataItems, }); }; diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSubFieldOptionKey.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSubFieldOptionKey.ts new file mode 100644 index 000000000..1af94727d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSubFieldOptionKey.ts @@ -0,0 +1,17 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels'; + +export const getSubFieldOptionKey = ( + fieldMetadataItem: FieldMetadataItem, + subFieldName: string, +) => { + const subFieldNameLabelKey = `${subFieldName}Label`; + + const subFieldLabel = ( + (COMPOSITE_FIELD_IMPORT_LABELS as any)[fieldMetadataItem.type] as any + )[subFieldNameLabelKey]; + + const subFieldKey = `${subFieldLabel} (${fieldMetadataItem.name})`; + + return subFieldKey; +}; 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 d38985c40..feee50d9a 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/__mocks__/mockRsiValues.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/__mocks__/mockRsiValues.ts @@ -156,6 +156,7 @@ export const mockRsiValues = mockComponentBehaviourForTypes({ await sleep(4000, (resolve) => resolve(data)); return data; }, + availableFieldMetadataItems: [] }); export const editableTableInitialData = [ diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx deleted file mode 100644 index c157d3df0..000000000 --- a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import { createPortal } from 'react-dom'; -import { ReadonlyDeep } from 'type-fest'; -import { useDebouncedCallback } from 'use-debounce'; - -import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; -import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; -import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; -import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; -import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; -import { useLingui } from '@lingui/react/macro'; -import { AppTooltip } from 'twenty-ui/display'; -import { SelectOption } from 'twenty-ui/input'; -import { MenuItem, MenuItemSelect } from 'twenty-ui/navigation'; -import { v4 } from 'uuid'; -import { useUpdateEffect } from '~/hooks/useUpdateEffect'; - -interface MatchColumnSelectProps { - columnIndex: string; - onChange: (value: ReadonlyDeep | null) => void; - value?: ReadonlyDeep; - options: readonly ReadonlyDeep[]; - placeholder?: string; -} - -export const MatchColumnSelect = ({ - onChange, - value, - options: initialOptions, - placeholder, - columnIndex, -}: MatchColumnSelectProps) => { - const dropdownId = `match-column-select-dropdown-${columnIndex}`; - - const { closeDropdown } = useDropdown(dropdownId); - - const [searchFilter, setSearchFilter] = useState(''); - const [options, setOptions] = useState(initialOptions); - - const handleSearchFilterChange = useCallback( - (text: string) => { - setOptions( - initialOptions.filter((option) => - option.label.toLowerCase().includes(text.toLowerCase()), - ), - ); - }, - [initialOptions], - ); - - const debouncedHandleSearchFilter = useDebouncedCallback( - handleSearchFilterChange, - 100, - { - leading: true, - }, - ); - - const handleFilterChange = (event: React.ChangeEvent) => { - const value = event.currentTarget.value; - - setSearchFilter(value); - debouncedHandleSearchFilter(value); - }; - - const handleChange = (option: ReadonlyDeep) => { - onChange(option); - closeDropdown(); - }; - - useUpdateEffect(() => { - setOptions(initialOptions); - }, [initialOptions]); - - const { t } = useLingui(); - - return ( - - } - dropdownComponents={ - <> - - - - {options?.map((option) => { - const id = `${v4()}-${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/components/MatchColumnSelectFieldSelectDropdownContent.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent.tsx new file mode 100644 index 000000000..ca32f68bd --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent.tsx @@ -0,0 +1,103 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField'; +import { DO_NOT_IMPORT_OPTION_KEY } from '@/spreadsheet-import/constants/DoNotImportOptionKey'; +import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; +import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; +import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { useLingui } from '@lingui/react/macro'; +import { useState } from 'react'; +import { IconForbid, IconX, useIcons } from 'twenty-ui/display'; +import { SelectOption } from 'twenty-ui/input'; +import { MenuItemSelect } from 'twenty-ui/navigation'; +import { ReadonlyDeep } from 'type-fest'; + +export const MatchColumnSelectFieldSelectDropdownContent = ({ + selectedValue, + onSelectFieldMetadataItem, + onCancelSelect, + onDoNotImportSelect, + options, +}: { + selectedValue: SelectOption | undefined; + onSelectFieldMetadataItem: ( + selectedFieldMetadataItem: FieldMetadataItem, + ) => void; + onCancelSelect: () => void; + onDoNotImportSelect: () => void; + options: readonly ReadonlyDeep[]; +}) => { + const [searchFilter, setSearchFilter] = useState(''); + + const handleFilterChange = (event: React.ChangeEvent) => { + const value = event.currentTarget.value; + + setSearchFilter(value); + }; + + const { availableFieldMetadataItems } = useSpreadsheetImportInternal(); + + const filteredAvailableFieldMetadataItems = + availableFieldMetadataItems.filter( + (field) => + field.label.toLowerCase().includes(searchFilter.toLowerCase()) || + field.name.toLowerCase().includes(searchFilter.toLowerCase()), + ); + + const { getIcon } = useIcons(); + + const handleFieldClick = (fieldMetadataItem: FieldMetadataItem) => { + onSelectFieldMetadataItem(fieldMetadataItem); + }; + + const handleCancelClick = () => { + onCancelSelect(); + }; + + const { t } = useLingui(); + + return ( + <> + + } + > + Select matching field + + + + + + {filteredAvailableFieldMetadataItems.map((field) => ( + handleFieldClick(field)} + disabled={ + options.find((option) => option.value === field.name)?.disabled && + selectedValue?.value !== field.name + } + LeftIcon={getIcon(field.icon)} + text={field.label} + hasSubMenu={isCompositeField(field.type)} + /> + ))} + + + ); +}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx new file mode 100644 index 000000000..243cfb9a8 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx @@ -0,0 +1,109 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField'; +import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey'; +import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; +import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; +import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { useState } from 'react'; +import { isDefined } from 'twenty-shared/utils'; +import { + IconChevronLeft, + OverflowingTextWithTooltip, + useIcons, +} from 'twenty-ui/display'; +import { SelectOption } from 'twenty-ui/input'; +import { MenuItem } from 'twenty-ui/navigation'; +import { ReadonlyDeep } from 'type-fest'; + +export const MatchColumnSelectSubFieldSelectDropdownContent = ({ + fieldMetadataItem, + onSubFieldSelect, + options, + onBack, +}: { + fieldMetadataItem: FieldMetadataItem; + onSubFieldSelect: (subFieldNameSelected: string) => void; + options: readonly ReadonlyDeep[]; + onBack: () => void; +}) => { + const [searchFilter, setSearchFilter] = useState(''); + + const { getIcon } = useIcons(); + + const handleFilterChange = (event: React.ChangeEvent) => { + const value = event.currentTarget.value; + + setSearchFilter(value); + }; + + const handleSubFieldSelect = (subFieldName: string) => { + onSubFieldSelect(subFieldName); + }; + + const handleSubMenuBack = () => { + setSearchFilter(''); + onBack(); + }; + + if (!isCompositeField(fieldMetadataItem.type)) { + return <>; + } + + const fieldMetadataItemSettings = + SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldMetadataItem.type]; + + const subFieldNamesThatExistInOptions = fieldMetadataItemSettings.subFields + .filter((subFieldName) => { + const optionKey = getSubFieldOptionKey(fieldMetadataItem, subFieldName); + + const correspondingOption = options.find( + (option) => option.value === optionKey, + ); + + return isDefined(correspondingOption); + }) + .filter((subFieldName) => subFieldName.includes(searchFilter)); + + return ( + <> + + } + > + + + + + + {subFieldNamesThatExistInOptions.map((subFieldName) => ( + handleSubFieldSelect(subFieldName)} + LeftIcon={getIcon(fieldMetadataItem.icon)} + text={ + (fieldMetadataItemSettings.labelBySubField as any)[subFieldName] + } + disabled={ + options.find( + (option) => + option.value === + getSubFieldOptionKey(fieldMetadataItem, subFieldName), + )?.disabled + } + /> + ))} + + + ); +}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx new file mode 100644 index 000000000..57cf5ddc5 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx @@ -0,0 +1,144 @@ +import { useState } from 'react'; +import { ReadonlyDeep } from 'type-fest'; + +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField'; +import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey'; +import { MatchColumnSelectFieldSelectDropdownContent } from '@/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent'; +import { MatchColumnSelectSubFieldSelectDropdownContent } from '@/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent'; +import { DO_NOT_IMPORT_OPTION_KEY } from '@/spreadsheet-import/constants/DoNotImportOptionKey'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { isDefined } from 'twenty-shared/utils'; +import { SelectOption } from 'twenty-ui/input'; +import { MenuItem } from 'twenty-ui/navigation'; + +interface MatchColumnToFieldSelectProps { + columnIndex: string; + onChange: (value: ReadonlyDeep | null) => void; + value?: ReadonlyDeep; + options: readonly ReadonlyDeep[]; + placeholder?: string; +} + +export const MatchColumnToFieldSelect = ({ + onChange, + value, + options, + placeholder, + columnIndex, +}: MatchColumnToFieldSelectProps) => { + const dropdownId = `match-column-select-v2-dropdown-${columnIndex}`; + + const { closeDropdown } = useDropdown(dropdownId); + + const [selectedFieldMetadataItem, setSelectedFieldMetadataItem] = + useState(null); + + const handleFieldMetadataItemSelect = ( + selectedFieldMetadataItem: FieldMetadataItem, + ) => { + setSelectedFieldMetadataItem(selectedFieldMetadataItem); + + if (!isCompositeField(selectedFieldMetadataItem.type)) { + const correspondingOption = options.find( + (option) => option.value === selectedFieldMetadataItem.name, + ); + + if (isDefined(correspondingOption)) { + setSelectedFieldMetadataItem(null); + + onChange(correspondingOption); + closeDropdown(); + } + } + }; + + const handleSubFieldSelect = (subFieldNameSelected: string) => { + if (!isDefined(selectedFieldMetadataItem)) { + return; + } + + const correspondingOption = options.find((option) => { + const optionKey = getSubFieldOptionKey( + selectedFieldMetadataItem, + subFieldNameSelected, + ); + + return option.value === optionKey; + }); + + if (isDefined(correspondingOption)) { + setSelectedFieldMetadataItem(null); + + onChange(correspondingOption); + closeDropdown(); + } + }; + + const handleDoNotImportSelect = () => { + if (isDefined(doNotImportOption)) { + onChange(doNotImportOption); + closeDropdown(); + } + }; + + const handleClickOutside = () => { + setSelectedFieldMetadataItem(null); + }; + + const handleSubFieldBack = () => { + setSelectedFieldMetadataItem(null); + }; + + const handleCancelSelectClick = () => { + setSelectedFieldMetadataItem(null); + closeDropdown(); + }; + + const doNotImportOption = options.find( + (option) => option.value === DO_NOT_IMPORT_OPTION_KEY, + ); + + const shouldDisplaySubFieldMetadataItemSelect = isDefined( + selectedFieldMetadataItem?.type, + ) + ? isCompositeField(selectedFieldMetadataItem?.type) + : false; + + return ( + + } + dropdownComponents={ + shouldDisplaySubFieldMetadataItemSelect && selectedFieldMetadataItem ? ( + + ) : ( + + ) + } + onClickOutside={handleClickOutside} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx index cb90cb92b..519d5da57 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx @@ -5,8 +5,8 @@ import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpre import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager'; import { useStepBar } from '@/ui/navigation/step-bar/hooks/useStepBar'; import { useLingui } from '@lingui/react/macro'; -import { IconButton } from 'twenty-ui/input'; import { IconX } from 'twenty-ui/display'; +import { IconButton } from 'twenty-ui/input'; const StyledCloseButtonContainer = styled.div` align-items: center; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/constants/DoNotImportOptionKey.ts b/packages/twenty-front/src/modules/spreadsheet-import/constants/DoNotImportOptionKey.ts new file mode 100644 index 000000000..fdd1b3c84 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/constants/DoNotImportOptionKey.ts @@ -0,0 +1 @@ +export const DO_NOT_IMPORT_OPTION_KEY = 'do-not-import'; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx b/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx index bec306a77..725e689ec 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx @@ -45,6 +45,7 @@ export const mockedSpreadsheetOptions: SpreadsheetImportDialogOptions { 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 1cfc26ecd..56c6f0d8a 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 @@ -17,6 +17,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Modal } from '@/ui/layout/modal/components/Modal'; +import { DO_NOT_IMPORT_OPTION_KEY } from '@/spreadsheet-import/constants/DoNotImportOptionKey'; import { UnmatchColumn } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumn'; import { initialComputedColumnsSelector } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/states/initialComputedColumnsState'; import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep'; @@ -116,7 +117,7 @@ export const MatchColumnsStep = ({ const onChange = useCallback( (value: T, columnIndex: number) => { - if (value === 'do-not-import') { + if (value === DO_NOT_IMPORT_OPTION_KEY) { if (columns[columnIndex].type === SpreadsheetColumnType.ignored) { onRevertIgnore(columnIndex); } else { 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 deleted file mode 100644 index b232a2a67..000000000 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelect.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; - -import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; - -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 { Tag, TagColor } from 'twenty-ui/components'; -import { IconChevronDown } from 'twenty-ui/display'; -import { SelectOption } from 'twenty-ui/input'; - -const StyledContainer = styled.div` - align-items: center; - display: flex; - gap: ${({ theme }) => theme.spacing(4)}; - justify-content: space-between; - padding-bottom: ${({ theme }) => theme.spacing(1)}; -`; - -const StyledControlContainer = styled.div<{ cursor: string }>` - align-items: center; - background-color: ${({ theme }) => theme.background.transparent.lighter}; - border: 1px solid ${({ theme }) => theme.border.color.medium}; - box-sizing: border-box; - border-radius: ${({ theme }) => theme.border.radius.sm}; - color: ${({ theme }) => theme.font.color.primary}; - cursor: ${({ cursor }) => cursor}; - display: flex; - gap: ${({ theme }) => theme.spacing(1)}; - height: ${({ theme }) => theme.spacing(8)}; - justify-content: space-between; - padding: 0 ${({ theme }) => theme.spacing(2)}; - width: 100%; -`; - -const StyledLabel = styled.span` - color: ${({ theme }) => theme.font.color.primary}; - font-weight: ${({ theme }) => theme.font.weight.regular}; - font-size: ${({ theme }) => theme.font.size.md}; -`; - -const StyledControlLabel = styled.div` - align-items: center; - display: flex; - gap: ${({ theme }) => theme.spacing(1)}; -`; - -const StyledIconChevronDown = styled(IconChevronDown)` - color: ${({ theme }) => theme.font.color.tertiary}; -`; - -interface SubMatchingSelectProps { - option: SpreadsheetMatchedOptions | Partial>; - column: - | SpreadsheetMatchedSelectColumn - | SpreadsheetMatchedSelectOptionsColumn; - onSubChange: (val: T, index: number, option: string) => void; - placeholder: string; - selectedOption?: - | SpreadsheetMatchedOptions - | Partial>; -} - -export const SubMatchingSelect = ({ - option, - column, - onSubChange, - placeholder, -}: SubMatchingSelectProps) => { - const { fields } = useSpreadsheetImportInternal(); - const options = getFieldOptions(fields, column.value) as SelectOption[]; - const value = options.find((opt) => opt.value === option.value); - const [isOpen, setIsOpen] = useState(false); - - const theme = useTheme(); - - const handleSelect = (selectedOption: SelectOption) => { - onSubChange(selectedOption.value as T, column.index, option.entry ?? ''); - setIsOpen(false); - }; - - const setHotkeyScope = useSetHotkeyScope(); - - useEffect(() => { - setHotkeyScope(SelectFieldHotkeyScope.SelectField); - }, [setHotkeyScope]); - - return ( - - - - {option.entry} - - - - setIsOpen(!isOpen)} - id="control" - > - - - {isOpen && ( - setIsOpen(false)} - hotkeyScope={SelectFieldHotkeyScope.SelectField} - /> - )} - - - ); -}; 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 a48838856..6c59cfb71 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 @@ -1,6 +1,7 @@ import styled from '@emotion/styled'; -import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect'; +import { MatchColumnToFieldSelect } from '@/spreadsheet-import/components/MatchColumnToFieldSelect'; +import { DO_NOT_IMPORT_OPTION_KEY } from '@/spreadsheet-import/constants/DoNotImportOptionKey'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType'; import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns'; @@ -54,7 +55,7 @@ export const TemplateColumn = ({ const selectOptions = [ { Icon: IconForbid, - value: 'do-not-import', + value: DO_NOT_IMPORT_OPTION_KEY, label: t`Do not import`, }, ...fieldOptions, @@ -65,12 +66,12 @@ export const TemplateColumn = ({ ); const ignoreValue = selectOptions.find( - ({ value }) => value === 'do-not-import', + ({ value }) => value === DO_NOT_IMPORT_OPTION_KEY, ); return ( - onChange(value?.value as T, column.index)} 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 4c4daedef..41105d3c0 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 @@ -9,11 +9,10 @@ import { } from '@/spreadsheet-import/types'; import { TextInput } from '@/ui/input/components/TextInput'; -import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect'; import { isDefined } from 'twenty-shared/utils'; -import { ImportedStructuredRowMetadata } from '../types'; import { AppTooltip } from 'twenty-ui/display'; import { Checkbox, CheckboxVariant, Toggle } from 'twenty-ui/input'; +import { ImportedStructuredRowMetadata } from '../types'; const StyledHeaderContainer = styled.div` align-items: center; @@ -61,6 +60,10 @@ const StyledDefaultContainer = styled.div` text-overflow: ellipsis; `; +const StyledSelectReadonlyValueContianer = styled.div` + padding-left: ${({ theme }) => theme.spacing(2)}; +`; + const SELECT_COLUMN_KEY = 'select-row'; export const generateColumns = ( @@ -130,26 +133,10 @@ export const generateColumns = ( switch (column.fieldType.type) { case 'select': { - const value = column.fieldType.options.find( - (option) => option.value === (row[columnKey] as string), - ); - component = ( - { - onRowChange({ ...row, [columnKey]: value?.value }, true); - }} - options={column.fieldType.options} - columnIndex={column.key} - /> + + {row[columnKey]} + ); break; } diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportDialogOptions.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportDialogOptions.ts index 62230c8be..2d6b2d1a3 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportDialogOptions.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportDialogOptions.ts @@ -1,8 +1,9 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns'; import { SpreadsheetImportFields } from '@/spreadsheet-import/types/SpreadsheetImportFields'; +import { SpreadsheetImportImportValidationResult } from '@/spreadsheet-import/types/SpreadsheetImportImportValidationResult'; 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'; @@ -58,4 +59,5 @@ export type SpreadsheetImportDialogOptions = { rtl?: boolean; // Allow header selection selectHeader?: boolean; + availableFieldMetadataItems: FieldMetadataItem[]; };