From 312632e686a9c2456eecb29371e6867b5d92701a Mon Sep 17 00:00:00 2001
From: Etienne <45695613+etiennejouan@users.noreply.github.com>
Date: Fri, 13 Jun 2025 15:43:16 +0200
Subject: [PATCH] update import auto matching (#12552)
closes https://github.com/twentyhq/core-team-issues/issues/905
---
.../utils/getFieldMetadataTypeLabel.ts | 19 ++++
.../utils/getFilterableFieldTypeLabel.ts | 8 --
.../getSettingsNonCompositeFieldTypeLabels.ts | 10 --
...ColumnSelectFieldSelectDropdownContent.tsx | 102 +++++++++++++-----
...umnSelectSubFieldSelectDropdownContent.tsx | 20 +++-
.../components/MatchColumnToFieldSelect.tsx | 11 ++
...useComputeColumnSuggestionsAndAutoMatch.ts | 42 ++++++++
.../MatchColumnsStep/MatchColumnsStep.tsx | 21 +---
.../components/TemplateColumn.tsx | 32 +++---
.../suggestedFieldsByColumnHeaderState.ts | 7 ++
.../SelectHeaderStep/SelectHeaderStep.tsx | 11 ++
.../components/UploadStep/UploadStep.tsx | 10 ++
.../utils/getMatchedColumnsWithFuse.ts | 56 ++++++++--
.../utils/spreadsheetBuildFieldOptions.ts | 29 +++++
14 files changed, 283 insertions(+), 95 deletions(-)
create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getFieldMetadataTypeLabel.ts
delete mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel.ts
delete mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSettingsNonCompositeFieldTypeLabels.ts
create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/hooks/useComputeColumnSuggestionsAndAutoMatch.ts
create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/states/suggestedFieldsByColumnHeaderState.ts
create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetBuildFieldOptions.ts
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getFieldMetadataTypeLabel.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getFieldMetadataTypeLabel.ts
new file mode 100644
index 000000000..15d9732ff
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getFieldMetadataTypeLabel.ts
@@ -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;
+};
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel.ts
deleted file mode 100644
index ab73d2be5..000000000
--- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel.ts
+++ /dev/null
@@ -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;
-};
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSettingsNonCompositeFieldTypeLabels.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSettingsNonCompositeFieldTypeLabels.ts
deleted file mode 100644
index 67a40d164..000000000
--- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSettingsNonCompositeFieldTypeLabels.ts
+++ /dev/null
@@ -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;
-};
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent.tsx
index 2247ee06d..44268e052 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent.tsx
+++ b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent.tsx
@@ -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[];
+ 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 (
-
+
-
-
- {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={isCompositeFieldType(field.type)}
- />
- ))}
-
+
+
+ {!isNonEmptyString(searchFilter) && (
+ <>
+
+
+
+ {suggestedOptions.length > 0 && (
+ <>
+
+
+
+ {suggestedOptions.map((option) => (
+ handleSuggestedOptionClick(option)}
+ disabled={option.disabled}
+ LeftIcon={option.Icon}
+ text={option.label}
+ contextualText={option.fieldMetadataTypeLabel}
+ />
+ ))}
+
+ >
+ )}
+
+
+ >
+ )}
+
+ {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}
+ contextualText={getFieldMetadataTypeLabel(field.type)}
+ hasSubMenu={isCompositeFieldType(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
index e1c1eabdc..8a41b646c 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx
+++ b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx
@@ -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 (
-
+
handleSubFieldSelect(subFieldName)}
LeftIcon={getIcon(fieldMetadataItem.icon)}
- text={
- (fieldMetadataItemSettings.labelBySubField as any)[subFieldName]
- }
+ text={getCompositeSubFieldLabel(
+ fieldMetadataItem.type as CompositeFieldType,
+ subFieldName,
+ )}
disabled={
options.find(
(option) =>
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx
index 45c34a1c3..4cc66d240 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx
+++ b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnToFieldSelect.tsx
@@ -20,6 +20,7 @@ interface MatchColumnToFieldSelectProps {
onChange: (value: ReadonlyDeep | null) => void;
value?: ReadonlyDeep;
options: readonly ReadonlyDeep[];
+ suggestedOptions: readonly ReadonlyDeep[];
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 = ({
)
}
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/hooks/useComputeColumnSuggestionsAndAutoMatch.ts b/packages/twenty-front/src/modules/spreadsheet-import/hooks/useComputeColumnSuggestionsAndAutoMatch.ts
new file mode 100644
index 000000000..5640640da
--- /dev/null
+++ b/packages/twenty-front/src/modules/spreadsheet-import/hooks/useComputeColumnSuggestionsAndAutoMatch.ts
@@ -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 = () => {
+ const { fields, autoMapHeaders } = useSpreadsheetImportInternal();
+
+ 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;
+};
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 514cf7f95..d8db1b066 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
@@ -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 = ({
const { enqueueDialog } = useDialogManager();
const { enqueueSnackBar } = useSnackBar();
const dataExample = data.slice(0, 2);
- const { fields, autoMapHeaders } = useSpreadsheetImportInternal();
+ const { fields } = useSpreadsheetImportInternal();
const [isLoading, setIsLoading] = useState(false);
const [columns, setColumns] = useRecoilState(
initialComputedColumnsSelector(headerValues),
@@ -256,22 +255,6 @@ export const MatchColumnsStep = ({
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(
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 6c59cfb71..f7232a204 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,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 = ({
onChange,
}: TemplateColumnProps) => {
const { fields } = useSpreadsheetImportInternal();
+ 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 = ({
value={isIgnored ? ignoreValue : selectValue}
onChange={(value) => onChange(value?.value as T, column.index)}
options={selectOptions}
+ suggestedOptions={suggestedFieldOptions}
columnIndex={column.index.toString()}
/>
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/states/suggestedFieldsByColumnHeaderState.ts b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/states/suggestedFieldsByColumnHeaderState.ts
new file mode 100644
index 000000000..8ba18b3ed
--- /dev/null
+++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/states/suggestedFieldsByColumnHeaderState.ts
@@ -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[]>,
+});
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep.tsx
index a06a0dc65..238ee79ed 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep.tsx
+++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep.tsx
@@ -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) => {
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,
],
);
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/UploadStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/UploadStep.tsx
index 6119bb189..f1cba4fe7 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/UploadStep.tsx
+++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/UploadStep.tsx
@@ -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,
],
);
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumnsWithFuse.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumnsWithFuse.ts
index 7a3748285..0a3bab326 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumnsWithFuse.ts
+++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/getMatchedColumnsWithFuse.ts
@@ -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 = (
- columns: SpreadsheetColumns,
- fields: SpreadsheetImportFields,
- data: MatchColumnsStepProps['data'],
-) => {
+export const getMatchedColumnsWithFuse = ({
+ columns,
+ fields,
+ data,
+}: {
+ columns: SpreadsheetColumns;
+ fields: SpreadsheetImportFields;
+ data: MatchColumnsStepProps['data'];
+}) => {
const matchedColumns: SpreadsheetColumn[] = [];
const fieldsToSearch = new Fuse(fields, {
keys: ['label'],
includeScore: true,
+ ignoreLocation: true,
threshold: 0.3,
});
+ const suggestedFieldsByColumnHeader: Record<
+ SpreadsheetColumn['header'],
+ SpreadsheetImportField[]
+ > = {};
+
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,
+ );
+
+ if (isFirstMatchValid && isFieldStillUnmatched) {
+ const newColumn = setColumn(column, firstMatch.item as any, data);
matchedColumns.push(newColumn);
} else {
@@ -34,5 +70,5 @@ export const getMatchedColumnsWithFuse = (
}
}
- return { matchedColumns };
+ return { matchedColumns, suggestedFieldsByColumnHeader };
};
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetBuildFieldOptions.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetBuildFieldOptions.ts
new file mode 100644
index 000000000..359eb7910
--- /dev/null
+++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/spreadsheetBuildFieldOptions.ts
@@ -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 = (
+ fields: SpreadsheetImportFields,
+ columns: SpreadsheetColumns,
+) => {
+ 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;
+ });
+};