Improve CSV import sub-field selection (#11601)

This PR adds a better UX for selecting sub-fields when importing CSV
files.

Before : 

<img width="395" alt="image"
src="https://github.com/user-attachments/assets/5a599e7d-ed07-4531-8306-9d70e7cfa37d"
/>

After : 

<img width="298" alt="image"
src="https://github.com/user-attachments/assets/2be8a1df-d089-4341-970e-6db2b269141e"
/>

<img width="296" alt="image"
src="https://github.com/user-attachments/assets/584285f4-4e71-4abd-9adf-11819cab0dc5"
/>

- 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 <charles@twenty.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
Lucas Bordeau
2025-04-16 15:08:24 +02:00
committed by GitHub
parent b1c0613514
commit bf704bd1bc
17 changed files with 409 additions and 302 deletions

View File

@ -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<SelectOption> | null) => void;
value?: ReadonlyDeep<SelectOption>;
options: readonly ReadonlyDeep<SelectOption>[];
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<FieldMetadataItem | null>(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 (
<Dropdown
dropdownId={dropdownId}
dropdownHotkeyScope={{
scope: dropdownId,
}}
dropdownPlacement="bottom-start"
clickableComponent={
<MenuItem
LeftIcon={value?.Icon}
text={value?.label ?? placeholder ?? ''}
accent={value?.label ? 'default' : 'placeholder'}
/>
}
dropdownComponents={
shouldDisplaySubFieldMetadataItemSelect && selectedFieldMetadataItem ? (
<MatchColumnSelectSubFieldSelectDropdownContent
fieldMetadataItem={selectedFieldMetadataItem}
onSubFieldSelect={handleSubFieldSelect}
options={options}
onBack={handleSubFieldBack}
/>
) : (
<MatchColumnSelectFieldSelectDropdownContent
selectedValue={value}
onSelectFieldMetadataItem={handleFieldMetadataItemSelect}
onCancelSelect={handleCancelSelectClick}
onDoNotImportSelect={handleDoNotImportSelect}
options={options}
/>
)
}
onClickOutside={handleClickOutside}
/>
);
};