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:
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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) =>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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(
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>[]>,
|
||||
});
|
||||
@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@ -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 };
|
||||
};
|
||||
|
||||
@ -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;
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user