update import auto matching (#12552)

<img width="800" alt="Screenshot 2025-06-11 at 17 45 13"
src="https://github.com/user-attachments/assets/ecc04d41-d74a-424a-9f83-14a793cf4268"
/>

closes https://github.com/twentyhq/core-team-issues/issues/905
This commit is contained in:
Etienne
2025-06-13 15:43:16 +02:00
committed by GitHub
parent 57d002d79a
commit 312632e686
14 changed files with 283 additions and 95 deletions

View File

@ -0,0 +1,19 @@
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { isNonCompositeField } from '@/object-record/object-filter-dropdown/utils/isNonCompositeField';
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
import { SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const getFieldMetadataTypeLabel = (fieldType: FieldMetadataType) => {
//TODO: Remove ?.label > .label when we have a proper type for field (issue #1097)
if (
isNonCompositeField(fieldType) ||
fieldType === FieldMetadataType.RELATION
)
return SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS[
fieldType as keyof typeof SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS
]?.label;
if (isCompositeFieldType(fieldType))
return SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldType]?.label;
};

View File

@ -1,8 +0,0 @@
import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs';
export const getFilterableFieldTypeLabel = (
filterableFieldType: FilterableFieldType,
) => {
return SETTINGS_FIELD_TYPE_CONFIGS[filterableFieldType].label;
};

View File

@ -1,10 +0,0 @@
import { SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs';
import { SettingsNonCompositeFieldType } from '@/settings/data-model/types/SettingsNonCompositeFieldType';
export const getSettingsNonCompositeFieldTypeLabel = (
settingsNonCompositeFieldType: SettingsNonCompositeFieldType,
) => {
return SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS[
settingsNonCompositeFieldType
].label;
};

View File

@ -1,4 +1,5 @@
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';
@ -7,28 +8,44 @@ import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenu
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,
options,
suggestedOptions,
}: {
selectedValue: SelectOption | undefined;
onSelectFieldMetadataItem: (
selectedFieldMetadataItem: FieldMetadataItem,
) => void;
onSelectSuggestedOption: (selectedSuggestedOption: SelectOption) => void;
onCancelSelect: () => void;
onDoNotImportSelect: () => void;
options: readonly ReadonlyDeep<SelectOption>[];
options: readonly ReadonlyDeep<
SelectOption & { fieldMetadataTypeLabel?: string }
>[];
suggestedOptions: readonly ReadonlyDeep<
SelectOption & { fieldMetadataTypeLabel?: string }
>[];
}) => {
const [searchFilter, setSearchFilter] = useState('');
@ -53,6 +70,10 @@ export const MatchColumnSelectFieldSelectDropdownContent = ({
onSelectFieldMetadataItem(fieldMetadataItem);
};
const handleSuggestedOptionClick = (suggestedOption: SelectOption) => {
onSelectSuggestedOption(suggestedOption);
};
const handleCancelClick = () => {
onCancelSelect();
};
@ -60,7 +81,7 @@ export const MatchColumnSelectFieldSelectDropdownContent = ({
const { t } = useLingui();
return (
<DropdownContent>
<DropdownContent widthInPixels={320}>
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
@ -75,30 +96,63 @@ export const MatchColumnSelectFieldSelectDropdownContent = ({
value={searchFilter}
onChange={handleFilterChange}
autoFocus
placeholder={t`Search fields`}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
<MenuItemSelect
selected={selectedValue?.value === DO_NOT_IMPORT_OPTION_KEY}
onClick={onDoNotImportSelect}
LeftIcon={IconForbid}
text={t`Do not import`}
/>
{filteredAvailableFieldMetadataItems.map((field) => (
<MenuItemSelect
key={field.id}
selected={selectedValue?.value === field.name}
onClick={() => handleFieldClick(field)}
disabled={
options.find((option) => option.value === field.name)?.disabled &&
selectedValue?.value !== field.name
}
LeftIcon={getIcon(field.icon)}
text={field.label}
hasSubMenu={isCompositeFieldType(field.type)}
/>
))}
</DropdownMenuItemsContainer>
<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)}
disabled={option.disabled}
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)}
disabled={
options.find((option) => option.value === field.name)
?.disabled && selectedValue?.value !== field.name
}
LeftIcon={getIcon(field.icon)}
text={field.label}
contextualText={getFieldMetadataTypeLabel(field.type)}
hasSubMenu={isCompositeFieldType(field.type)}
/>
))}
</DropdownMenuItemsContainer>
</ScrollWrapper>
</StyledContainer>
</DropdownContent>
);
};

View File

@ -1,7 +1,9 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey';
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType';
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';
@ -66,10 +68,17 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({
return isDefined(correspondingOption);
})
.filter((subFieldName) => subFieldName.includes(searchFilter));
.filter((subFieldName) =>
getCompositeSubFieldLabel(
fieldMetadataItem.type as CompositeFieldType,
subFieldName,
)
.toLowerCase()
.includes(searchFilter.toLowerCase()),
);
return (
<DropdownContent>
<DropdownContent widthInPixels={320}>
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
@ -92,9 +101,10 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({
key={subFieldName}
onClick={() => handleSubFieldSelect(subFieldName)}
LeftIcon={getIcon(fieldMetadataItem.icon)}
text={
(fieldMetadataItemSettings.labelBySubField as any)[subFieldName]
}
text={getCompositeSubFieldLabel(
fieldMetadataItem.type as CompositeFieldType,
subFieldName,
)}
disabled={
options.find(
(option) =>

View File

@ -20,6 +20,7 @@ interface MatchColumnToFieldSelectProps {
onChange: (value: ReadonlyDeep<SelectOption> | null) => void;
value?: ReadonlyDeep<SelectOption>;
options: readonly ReadonlyDeep<SelectOption>[];
suggestedOptions: readonly ReadonlyDeep<SelectOption>[];
placeholder?: string;
}
@ -32,6 +33,7 @@ export const MatchColumnToFieldSelect = ({
onChange,
value,
options,
suggestedOptions,
placeholder,
columnIndex,
}: MatchColumnToFieldSelectProps) => {
@ -83,6 +85,13 @@ export const MatchColumnToFieldSelect = ({
}
};
const handleSelectSuggestedOption = (
selectedSuggestedOption: SelectOption,
) => {
onChange(selectedSuggestedOption);
closeDropdown();
};
const handleDoNotImportSelect = () => {
if (isDefined(doNotImportOption)) {
onChange(doNotImportOption);
@ -138,9 +147,11 @@ export const MatchColumnToFieldSelect = ({
<MatchColumnSelectFieldSelectDropdownContent
selectedValue={value}
onSelectFieldMetadataItem={handleFieldMetadataItemSelect}
onSelectSuggestedOption={handleSelectSuggestedOption}
onCancelSelect={handleCancelSelectClick}
onDoNotImportSelect={handleDoNotImportSelect}
options={options}
suggestedOptions={suggestedOptions}
/>
)
}

View File

@ -0,0 +1,42 @@
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import {
initialComputedColumnsSelector,
matchColumnsState,
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/states/initialComputedColumnsState';
import { suggestedFieldsByColumnHeaderState } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/states/suggestedFieldsByColumnHeaderState';
import { ImportedRow } from '@/spreadsheet-import/types';
import { getMatchedColumnsWithFuse } from '@/spreadsheet-import/utils/getMatchedColumnsWithFuse';
import { useRecoilCallback } from 'recoil';
export const useComputeColumnSuggestionsAndAutoMatch = <T extends string>() => {
const { fields, autoMapHeaders } = useSpreadsheetImportInternal<T>();
const computeColumnSuggestionsAndAutoMatch = useRecoilCallback(
({ set, snapshot }) =>
async ({
headerValues,
data,
}: {
headerValues: ImportedRow;
data: ImportedRow[];
}) => {
if (autoMapHeaders) {
const columns = snapshot
.getLoadable(initialComputedColumnsSelector(headerValues))
.getValue();
const { matchedColumns, suggestedFieldsByColumnHeader } =
getMatchedColumnsWithFuse({ columns, fields, data });
set(matchColumnsState, matchedColumns);
set(
suggestedFieldsByColumnHeaderState,
suggestedFieldsByColumnHeader,
);
}
},
[autoMapHeaders, fields],
);
return computeColumnSuggestionsAndAutoMatch;
};

View File

@ -1,5 +1,5 @@
import styled from '@emotion/styled';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
@ -27,7 +27,6 @@ import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn'
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
import { SpreadsheetImportField } from '@/spreadsheet-import/types/SpreadsheetImportField';
import { getMatchedColumnsWithFuse } from '@/spreadsheet-import/utils/getMatchedColumnsWithFuse';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { Trans, useLingui } from '@lingui/react/macro';
import { useRecoilState } from 'recoil';
@ -80,7 +79,7 @@ export const MatchColumnsStep = <T extends string>({
const { enqueueDialog } = useDialogManager();
const { enqueueSnackBar } = useSnackBar();
const dataExample = data.slice(0, 2);
const { fields, autoMapHeaders } = useSpreadsheetImportInternal<T>();
const { fields } = useSpreadsheetImportInternal<T>();
const [isLoading, setIsLoading] = useState(false);
const [columns, setColumns] = useRecoilState(
initialComputedColumnsSelector(headerValues),
@ -256,22 +255,6 @@ export const MatchColumnsStep = <T extends string>({
t,
]);
useEffect(() => {
const isInitialColumnsState = columns.every(
(column) => column.type === SpreadsheetColumnType.empty,
);
if (autoMapHeaders && isInitialColumnsState) {
const { matchedColumns } = getMatchedColumnsWithFuse(
columns,
fields,
data,
);
setColumns(matchedColumns);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const hasMatchedColumns = columns.some(
(column) =>
![SpreadsheetColumnType.ignored, SpreadsheetColumnType.empty].includes(

View File

@ -3,10 +3,12 @@ import styled from '@emotion/styled';
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 { suggestedFieldsByColumnHeaderState } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/states/suggestedFieldsByColumnHeaderState';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
import { spreadsheetBuildFieldOptions } from '@/spreadsheet-import/utils/spreadsheetBuildFieldOptions';
import { useLingui } from '@lingui/react/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import { useRecoilValue } from 'recoil';
import { IconForbid } from 'twenty-ui/display';
const StyledContainer = styled.div`
@ -28,29 +30,20 @@ export const TemplateColumn = <T extends string>({
onChange,
}: TemplateColumnProps<T>) => {
const { fields } = useSpreadsheetImportInternal<T>();
const suggestedFieldsByColumnHeader = useRecoilValue(
suggestedFieldsByColumnHeaderState,
);
const column = columns[columnIndex];
const isIgnored = column.type === SpreadsheetColumnType.ignored;
const { t } = useLingui();
const fieldOptions = fields
.filter((field) => field.fieldMetadataType !== FieldMetadataType.RICH_TEXT)
.map(({ Icon, label, key }) => {
const isSelected =
columns.findIndex((column) => {
if ('value' in column) {
return column.value === key;
}
return false;
}) !== -1;
return {
Icon: Icon,
value: key,
label: label,
disabled: isSelected,
} as const;
});
const fieldOptions = spreadsheetBuildFieldOptions(fields, columns);
const suggestedFieldOptions = spreadsheetBuildFieldOptions(
suggestedFieldsByColumnHeader[column.header] ?? [],
columns,
);
const selectOptions = [
{
@ -76,6 +69,7 @@ export const TemplateColumn = <T extends string>({
value={isIgnored ? ignoreValue : selectValue}
onChange={(value) => onChange(value?.value as T, column.index)}
options={selectOptions}
suggestedOptions={suggestedFieldOptions}
columnIndex={column.index.toString()}
/>
</StyledContainer>

View File

@ -0,0 +1,7 @@
import { SpreadsheetImportField } from '@/spreadsheet-import/types';
import { createState } from 'twenty-ui/utilities';
export const suggestedFieldsByColumnHeaderState = createState({
key: 'suggestedFieldsByColumnHeaderState',
defaultValue: {} as Record<string, SpreadsheetImportField<string>[]>,
});

View File

@ -7,6 +7,7 @@ import { ImportedRow } from '@/spreadsheet-import/types';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { useComputeColumnSuggestionsAndAutoMatch } from '@/spreadsheet-import/hooks/useComputeColumnSuggestionsAndAutoMatch';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep';
import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType';
@ -50,11 +51,20 @@ export const SelectHeaderStep = ({
const { selectHeaderStepHook } = useSpreadsheetImportInternal();
const computeColumnSuggestionsAndAutoMatch =
useComputeColumnSuggestionsAndAutoMatch();
const handleContinue = useCallback(
async (...args: Parameters<typeof selectHeaderStepHook>) => {
try {
const { importedRows: data, headerRow: headerValues } =
await selectHeaderStepHook(...args);
await computeColumnSuggestionsAndAutoMatch({
headerValues,
data,
});
setCurrentStepState({
type: SpreadsheetImportStepType.matchColumns,
data,
@ -73,6 +83,7 @@ export const SelectHeaderStep = ({
setPreviousStepState,
setCurrentStepState,
currentStepState,
computeColumnSuggestionsAndAutoMatch,
],
);

View File

@ -4,6 +4,7 @@ import { WorkBook } from 'xlsx-ugnis';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { useComputeColumnSuggestionsAndAutoMatch } from '@/spreadsheet-import/hooks/useComputeColumnSuggestionsAndAutoMatch';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep';
import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType';
@ -36,6 +37,9 @@ export const UploadStep = ({
const { maxRecords, uploadStepHook, selectHeaderStepHook, selectHeader } =
useSpreadsheetImportInternal();
const computeColumnSuggestionsAndAutoMatch =
useComputeColumnSuggestionsAndAutoMatch();
const handleContinue = useCallback(
async (workbook: WorkBook, file: File) => {
setUploadedFile(file);
@ -63,6 +67,11 @@ export const UploadStep = ({
const { importedRows: data, headerRow: headerValues } =
await selectHeaderStepHook(mappedWorkbook[0], trimmedData);
await computeColumnSuggestionsAndAutoMatch({
headerValues,
data,
});
setCurrentStepState({
type: SpreadsheetImportStepType.matchColumns,
data,
@ -92,6 +101,7 @@ export const UploadStep = ({
setUploadedFile,
currentStepState,
uploadStepHook,
computeColumnSuggestionsAndAutoMatch,
],
);

View File

@ -1,32 +1,68 @@
import { MatchColumnsStepProps } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
import {
SpreadsheetImportField,
SpreadsheetImportFields,
} 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 { setColumn } from '@/spreadsheet-import/utils/setColumn';
import Fuse from 'fuse.js';
import { isDefined } from 'twenty-shared/utils';
export const getMatchedColumnsWithFuse = <T extends string>(
columns: SpreadsheetColumns<T>,
fields: SpreadsheetImportFields<T>,
data: MatchColumnsStepProps['data'],
) => {
export const getMatchedColumnsWithFuse = <T extends string>({
columns,
fields,
data,
}: {
columns: SpreadsheetColumns<T>;
fields: SpreadsheetImportFields<T>;
data: MatchColumnsStepProps['data'];
}) => {
const matchedColumns: SpreadsheetColumn<T>[] = [];
const fieldsToSearch = new Fuse(fields, {
keys: ['label'],
includeScore: true,
ignoreLocation: true,
threshold: 0.3,
});
const suggestedFieldsByColumnHeader: Record<
SpreadsheetColumn<T>['header'],
SpreadsheetImportField<T>[]
> = {};
for (const column of columns) {
const fieldsThatMatch = fieldsToSearch.search(column.header);
const firstMatch = fieldsThatMatch[0]?.item ?? null;
const firstMatch = fieldsThatMatch[0] || null;
const secondMatch = fieldsThatMatch[1] || null;
if (isDefined(firstMatch)) {
const newColumn = setColumn(column, firstMatch as any, data);
const isFirstMatchValid =
isDefined(firstMatch?.item) &&
isDefined(firstMatch?.score) &&
firstMatch.score < 0.4 &&
((isDefined(secondMatch?.score) &&
secondMatch.score !== firstMatch.score) ||
!isDefined(secondMatch));
const isFieldStillUnmatched = !matchedColumns.some(
(matchedColumn) =>
(matchedColumn.type === SpreadsheetColumnType.matched ||
matchedColumn.type === SpreadsheetColumnType.matchedCheckbox ||
matchedColumn.type === SpreadsheetColumnType.matchedSelect ||
matchedColumn.type === SpreadsheetColumnType.matchedSelectOptions) &&
matchedColumn?.value === firstMatch?.item?.key,
);
suggestedFieldsByColumnHeader[column.header] = fieldsThatMatch.map(
(match) => match.item as SpreadsheetImportField<T>,
);
if (isFirstMatchValid && isFieldStillUnmatched) {
const newColumn = setColumn(column, firstMatch.item as any, data);
matchedColumns.push(newColumn);
} else {
@ -34,5 +70,5 @@ export const getMatchedColumnsWithFuse = <T extends string>(
}
}
return { matchedColumns };
return { matchedColumns, suggestedFieldsByColumnHeader };
};

View File

@ -0,0 +1,29 @@
import { getFieldMetadataTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFieldMetadataTypeLabel';
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
import { FieldMetadataType } from 'twenty-shared/types';
export const spreadsheetBuildFieldOptions = <T extends string>(
fields: SpreadsheetImportFields<T>,
columns: SpreadsheetColumns<string>,
) => {
return fields
.filter((field) => field.fieldMetadataType !== FieldMetadataType.RICH_TEXT)
.map(({ Icon, label, key, fieldMetadataType }) => {
const isSelected =
columns.findIndex((column) => {
if ('value' in column) {
return column.value === key;
}
return false;
}) !== -1;
return {
Icon: Icon,
value: key,
label: label,
disabled: isSelected,
fieldMetadataTypeLabel: getFieldMetadataTypeLabel(fieldMetadataType),
} as const;
});
};