Files
twenty_crm/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent.tsx
Etienne 713d3defef Import - Upsert on composite fields (#12615)
To test : 
- Import a record with Id column (for upsert-ing) + some subfields in
each composite fields. Check that only matched subfields are updated
(Main issue)
- Import a record with a multi-select field - Check it works + Match
multi-select field on a non multi-select column, check it does not work.
(Specific bug fixed in second commit is : undefined value in multi
select column (corresponding to no item selected) caused error in
multi-select parsing).

closes https://github.com/twentyhq/core-team-issues/issues/990
2025-06-17 11:07:51 +02:00

150 lines
5.8 KiB
TypeScript

import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getFieldMetadataTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFieldMetadataTypeLabel';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { DO_NOT_IMPORT_OPTION_KEY } from '@/spreadsheet-import/constants/DoNotImportOptionKey';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
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 { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
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';
const StyledContainer = styled.div`
max-height: 360px;
`;
export const MatchColumnSelectFieldSelectDropdownContent = ({
selectedValue,
onSelectFieldMetadataItem,
onSelectSuggestedOption,
onCancelSelect,
onDoNotImportSelect,
suggestedOptions,
}: {
selectedValue: SelectOption | undefined;
onSelectFieldMetadataItem: (
selectedFieldMetadataItem: FieldMetadataItem,
) => void;
onSelectSuggestedOption: (selectedSuggestedOption: SelectOption) => void;
onCancelSelect: () => void;
onDoNotImportSelect: () => void;
suggestedOptions: readonly ReadonlyDeep<
SelectOption & { fieldMetadataTypeLabel?: string }
>[];
}) => {
const [searchFilter, setSearchFilter] = useState('');
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
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 handleSuggestedOptionClick = (suggestedOption: SelectOption) => {
onSelectSuggestedOption(suggestedOption);
};
const handleCancelClick = () => {
onCancelSelect();
};
const { t } = useLingui();
return (
<DropdownContent widthInPixels={320}>
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
onClick={handleCancelClick}
Icon={IconX}
/>
}
>
Select matching field
</DropdownMenuHeader>
<DropdownMenuSearchInput
value={searchFilter}
onChange={handleFilterChange}
autoFocus
placeholder={t`Search fields`}
/>
<DropdownMenuSeparator />
<StyledContainer>
<ScrollWrapper componentInstanceId="match-column-select-field-select-dropdown-content">
{!isNonEmptyString(searchFilter) && (
<>
<DropdownMenuItemsContainer scrollable={false}>
<MenuItemSelect
selected={selectedValue?.value === DO_NOT_IMPORT_OPTION_KEY}
onClick={onDoNotImportSelect}
LeftIcon={IconForbid}
text={t`Do not import`}
/>
</DropdownMenuItemsContainer>
{suggestedOptions.length > 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuSectionLabel label={t`Suggested`} />
<DropdownMenuItemsContainer scrollable={false}>
{suggestedOptions.map((option) => (
<MenuItemSelect
key={option.value}
selected={selectedValue?.value === option.value}
onClick={() => handleSuggestedOptionClick(option)}
LeftIcon={option.Icon}
text={option.label}
contextualText={option.fieldMetadataTypeLabel}
/>
))}
</DropdownMenuItemsContainer>
</>
)}
<DropdownMenuSeparator />
<DropdownMenuSectionLabel label={t`All fields`} />
</>
)}
<DropdownMenuItemsContainer scrollable={false}>
{filteredAvailableFieldMetadataItems.map((field) => (
<MenuItemSelect
key={field.id}
selected={selectedValue?.value === field.name}
onClick={() => handleFieldClick(field)}
LeftIcon={getIcon(field.icon)}
text={field.label}
contextualText={getFieldMetadataTypeLabel(field.type)}
hasSubMenu={isCompositeFieldType(field.type)}
/>
))}
</DropdownMenuItemsContainer>
</ScrollWrapper>
</StyledContainer>
</DropdownContent>
);
};