Changed the auto matching of columns in import (#12181)

This PR changes the way we do automatching in the import feature.

It uses [Fuse.js](https://www.fusejs.io/) to do a fuzzy text search on
fields and sub-fields.

The labels of sub-fields are now derived from the common config constant
we have for sub-fields.
This commit is contained in:
Lucas Bordeau
2025-05-23 18:33:18 +02:00
committed by GitHub
parent f7ccb5d207
commit 371fdba1f8
9 changed files with 121 additions and 46 deletions

View File

@ -6,7 +6,6 @@ import { StepNavigationButton } from '@/spreadsheet-import/components/StepNaviga
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { ImportedRow, ImportedStructuredRow } from '@/spreadsheet-import/types';
import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields';
import { getMatchedColumns } from '@/spreadsheet-import/utils/getMatchedColumns';
import { normalizeTableData } from '@/spreadsheet-import/utils/normalizeTableData';
import { setColumn } from '@/spreadsheet-import/utils/setColumn';
import { setIgnoreColumn } from '@/spreadsheet-import/utils/setIgnoreColumn';
@ -26,6 +25,7 @@ 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';
@ -82,8 +82,7 @@ export const MatchColumnsStep = <T extends string>({
const { enqueueDialog } = useDialogManager();
const { enqueueSnackBar } = useSnackBar();
const dataExample = data.slice(0, 2);
const { fields, autoMapHeaders, autoMapDistance } =
useSpreadsheetImportInternal<T>();
const { fields, autoMapHeaders } = useSpreadsheetImportInternal<T>();
const [isLoading, setIsLoading] = useState(false);
const [columns, setColumns] = useRecoilState(
initialComputedColumnsSelector(headerValues),
@ -264,7 +263,13 @@ export const MatchColumnsStep = <T extends string>({
(column) => column.type === SpreadsheetColumnType.empty,
);
if (autoMapHeaders && isInitialColumnsState) {
setColumns(getMatchedColumns(columns, fields, data, autoMapDistance));
const { matchedColumns } = getMatchedColumnsWithFuse(
columns,
fields,
data,
);
setColumns(matchedColumns);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@ -275,7 +280,7 @@ export const MatchColumnsStep = <T extends string>({
<StyledContent>
<Heading
title={t`Match Columns`}
description={t`Select the correct field for each column you'd like to import.`}
description={t`⚠️ Please verify the auto mapping of the columns. You can also ignore or change the mapping of the columns.`}
/>
<ColumnGrid
columns={columns}

View File

@ -20,7 +20,7 @@ const getExpandableContainerTitle = <T extends string>(
return `Match ${fieldLabel} (${
'matchedOptions' in column &&
column.matchedOptions.filter((option) => !isDefined(option.value)).length
column.matchedOptions?.filter((option) => !isDefined(option.value)).length
} Unmatched)`;
};
@ -70,7 +70,7 @@ export const UnmatchColumn = <T extends string>({
containAnimation
>
<StyledContentWrapper>
{column.matchedOptions.map((option) => (
{column.matchedOptions?.map((option) => (
<SubMatchingSelectRow
option={option}
column={column}

View File

@ -25,10 +25,10 @@ import {
// @ts-expect-error Todo: remove usage of react-data-grid`
import { RowsChangeData } from 'react-data-grid';
import { isDefined } from 'twenty-shared/utils';
import { IconTrash } from 'twenty-ui/display';
import { Button, Toggle } from 'twenty-ui/input';
import { generateColumns } from './components/columns';
import { ImportedStructuredRowMetadata } from './types';
import { Button, Toggle } from 'twenty-ui/input';
import { IconTrash } from 'twenty-ui/display';
const StyledContent = styled(Modal.Content)`
padding-left: ${({ theme }) => theme.spacing(6)};

View File

@ -0,0 +1,38 @@
import { MatchColumnsStepProps } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
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'],
) => {
const matchedColumns: SpreadsheetColumn<T>[] = [];
const fieldsToSearch = new Fuse(fields, {
keys: ['label'],
includeScore: true,
threshold: 0.3,
});
for (const column of columns) {
const fieldsThatMatch = fieldsToSearch.search(column.header);
const firstMatch = fieldsThatMatch[0]?.item ?? null;
if (isDefined(firstMatch)) {
const newColumn = setColumn(column, firstMatch as any, data);
matchedColumns.push(newColumn);
} else {
matchedColumns.push(column);
}
}
return { matchedColumns };
};