This reverts commit cc71394863.
Regression introduced in https://github.com/twentyhq/twenty/pull/13213
The import/export use an upsert logic and when it goes through the
"update" path it fails due to the connect not being implemented yet
(should be in https://github.com/twentyhq/core-team-issues/issues/1230)
---------
Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { defaultSpreadsheetImportProps } from '@/spreadsheet-import/provider/components/SpreadsheetImport';
|
||||
import {
|
||||
SpreadsheetImportDialogOptions,
|
||||
SpreadsheetImportFields
|
||||
SpreadsheetImportDialogOptions,
|
||||
SpreadsheetImportFields
|
||||
} from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
@@ -16,6 +16,7 @@ const fields = [
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
example: 'Stephanie',
|
||||
fieldValidationDefinitions: [
|
||||
{
|
||||
rule: 'required',
|
||||
@@ -23,8 +24,6 @@ const fields = [
|
||||
},
|
||||
],
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
fieldMetadataItemId: '1',
|
||||
isNestedField: false,
|
||||
},
|
||||
{
|
||||
Icon: null,
|
||||
@@ -34,6 +33,7 @@ const fields = [
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
example: 'McDonald',
|
||||
fieldValidationDefinitions: [
|
||||
{
|
||||
rule: 'unique',
|
||||
@@ -42,9 +42,6 @@ const fields = [
|
||||
},
|
||||
],
|
||||
description: 'Family / Last name',
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
fieldMetadataItemId: '2',
|
||||
isNestedField: false,
|
||||
},
|
||||
{
|
||||
Icon: null,
|
||||
@@ -54,6 +51,7 @@ const fields = [
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
example: '23',
|
||||
fieldValidationDefinitions: [
|
||||
{
|
||||
rule: 'regex',
|
||||
@@ -62,14 +60,12 @@ const fields = [
|
||||
level: 'warning',
|
||||
},
|
||||
],
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
fieldMetadataItemId: '3',
|
||||
isNestedField: false,
|
||||
},
|
||||
{
|
||||
Icon: null,
|
||||
label: 'Team',
|
||||
key: 'team',
|
||||
alternateMatches: ['department'],
|
||||
fieldType: {
|
||||
type: 'select',
|
||||
options: [
|
||||
@@ -77,31 +73,28 @@ const fields = [
|
||||
{ label: 'Team Two', value: 'two' },
|
||||
],
|
||||
},
|
||||
example: 'Team one',
|
||||
fieldValidationDefinitions: [
|
||||
{
|
||||
rule: 'required',
|
||||
errorMessage: 'Team is required',
|
||||
},
|
||||
],
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
fieldMetadataItemId: '4',
|
||||
isNestedField: false,
|
||||
},
|
||||
{
|
||||
Icon: null,
|
||||
label: 'Is manager',
|
||||
key: 'is_manager',
|
||||
alternateMatches: ['manages'],
|
||||
fieldType: {
|
||||
type: 'checkbox',
|
||||
booleanMatches: {},
|
||||
},
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
fieldMetadataItemId: '5',
|
||||
isNestedField: false,
|
||||
example: 'true',
|
||||
},
|
||||
] as SpreadsheetImportFields;
|
||||
] as SpreadsheetImportFields<string>;
|
||||
|
||||
export const importedColums: SpreadsheetColumns = [
|
||||
export const importedColums: SpreadsheetColumns<string> = [
|
||||
{
|
||||
header: 'Name',
|
||||
index: 0,
|
||||
@@ -128,13 +121,13 @@ export const importedColums: SpreadsheetColumns = [
|
||||
},
|
||||
];
|
||||
|
||||
const mockComponentBehaviourForTypes = (
|
||||
props: SpreadsheetImportDialogOptions,
|
||||
const mockComponentBehaviourForTypes = <T extends string>(
|
||||
props: SpreadsheetImportDialogOptions<T>,
|
||||
) => props;
|
||||
|
||||
export const mockRsiValues = mockComponentBehaviourForTypes({
|
||||
...defaultSpreadsheetImportProps,
|
||||
spreadsheetImportFields: fields,
|
||||
fields: fields,
|
||||
onSubmit: async () => {
|
||||
return;
|
||||
},
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
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 { hasNestedFields } from '@/spreadsheet-import/utils/spreadsheetImportHasNestedFields';
|
||||
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';
|
||||
@@ -139,7 +139,7 @@ export const MatchColumnSelectFieldSelectDropdownContent = ({
|
||||
LeftIcon={getIcon(field.icon)}
|
||||
text={field.label}
|
||||
contextualText={getFieldMetadataTypeLabel(field.type)}
|
||||
hasSubMenu={hasNestedFields(field)}
|
||||
hasSubMenu={isCompositeFieldType(field.type)}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
|
||||
+56
-22
@@ -1,7 +1,9 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SpreadsheetImportFieldOption } from '@/spreadsheet-import/types/SpreadsheetImportFieldOption';
|
||||
import { getSubFieldOptions } from '@/spreadsheet-import/utils/spreadsheetImportGetSubFieldOptions';
|
||||
import { hasNestedFields } from '@/spreadsheet-import/utils/spreadsheetImportHasNestedFields';
|
||||
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';
|
||||
@@ -10,8 +12,15 @@ import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/Dropdow
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { useState } from 'react';
|
||||
import { IconChevronLeft, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconChevronLeft,
|
||||
OverflowingTextWithTooltip,
|
||||
useIcons,
|
||||
} from 'twenty-ui/display';
|
||||
import { SelectOption } from 'twenty-ui/input';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import { ReadonlyDeep } from 'type-fest';
|
||||
|
||||
export const MatchColumnSelectSubFieldSelectDropdownContent = ({
|
||||
fieldMetadataItem,
|
||||
@@ -21,11 +30,13 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({
|
||||
}: {
|
||||
fieldMetadataItem: FieldMetadataItem;
|
||||
onSubFieldSelect: (subFieldNameSelected: string) => void;
|
||||
options: readonly Readonly<SpreadsheetImportFieldOption>[];
|
||||
options: readonly ReadonlyDeep<SelectOption>[];
|
||||
onBack: () => void;
|
||||
}) => {
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.value;
|
||||
|
||||
@@ -41,15 +52,31 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({
|
||||
onBack();
|
||||
};
|
||||
|
||||
if (!hasNestedFields(fieldMetadataItem)) {
|
||||
if (!isCompositeFieldType(fieldMetadataItem.type)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const subFieldOptions = getSubFieldOptions(
|
||||
fieldMetadataItem,
|
||||
options,
|
||||
searchFilter,
|
||||
);
|
||||
const fieldMetadataItemSettings =
|
||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldMetadataItem.type];
|
||||
|
||||
const subFieldsThatExistInOptions = fieldMetadataItemSettings.subFields
|
||||
.filter(({ subFieldName }) => {
|
||||
const optionKey = getSubFieldOptionKey(fieldMetadataItem, subFieldName);
|
||||
|
||||
const correspondingOption = options.find(
|
||||
(option) => option.value === optionKey,
|
||||
);
|
||||
|
||||
return isDefined(correspondingOption);
|
||||
})
|
||||
.filter(({ subFieldName }) =>
|
||||
getCompositeSubFieldLabel(
|
||||
fieldMetadataItem.type as CompositeFieldType,
|
||||
subFieldName,
|
||||
)
|
||||
.toLowerCase()
|
||||
.includes(searchFilter.toLowerCase()),
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
|
||||
@@ -70,17 +97,24 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{subFieldOptions.map(
|
||||
({ value, shortLabelForNestedField, Icon, disabled }) => (
|
||||
<MenuItem
|
||||
key={value}
|
||||
onClick={() => handleSubFieldSelect(value)}
|
||||
LeftIcon={Icon}
|
||||
text={shortLabelForNestedField}
|
||||
disabled={disabled}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{subFieldsThatExistInOptions.map(({ subFieldName }) => (
|
||||
<MenuItem
|
||||
key={subFieldName}
|
||||
onClick={() => handleSubFieldSelect(subFieldName)}
|
||||
LeftIcon={getIcon(fieldMetadataItem.icon)}
|
||||
text={getCompositeSubFieldLabel(
|
||||
fieldMetadataItem.type as CompositeFieldType,
|
||||
subFieldName,
|
||||
)}
|
||||
disabled={
|
||||
options.find(
|
||||
(option) =>
|
||||
option.value ===
|
||||
getSubFieldOptionKey(fieldMetadataItem, subFieldName),
|
||||
)?.disabled
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
);
|
||||
|
||||
+11
-7
@@ -3,11 +3,10 @@ import { ReadonlyDeep } from 'type-fest';
|
||||
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
||||
import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey';
|
||||
import { MatchColumnSelectFieldSelectDropdownContent } from '@/spreadsheet-import/components/MatchColumnSelectFieldSelectDropdownContent';
|
||||
import { MatchColumnSelectSubFieldSelectDropdownContent } from '@/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent';
|
||||
import { DO_NOT_IMPORT_OPTION_KEY } from '@/spreadsheet-import/constants/DoNotImportOptionKey';
|
||||
import { SpreadsheetImportFieldOption } from '@/spreadsheet-import/types/SpreadsheetImportFieldOption';
|
||||
import { hasNestedFields } from '@/spreadsheet-import/utils/spreadsheetImportHasNestedFields';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import styled from '@emotion/styled';
|
||||
@@ -20,7 +19,7 @@ interface MatchColumnToFieldSelectProps {
|
||||
columnIndex: string;
|
||||
onChange: (value: ReadonlyDeep<SelectOption> | null) => void;
|
||||
value?: ReadonlyDeep<SelectOption>;
|
||||
options: readonly Readonly<SpreadsheetImportFieldOption>[];
|
||||
options: readonly ReadonlyDeep<SelectOption>[];
|
||||
suggestedOptions: readonly ReadonlyDeep<SelectOption>[];
|
||||
placeholder?: string;
|
||||
}
|
||||
@@ -71,7 +70,12 @@ export const MatchColumnToFieldSelect = ({
|
||||
}
|
||||
|
||||
const correspondingOption = options.find((option) => {
|
||||
return option.value === subFieldNameSelected;
|
||||
const optionKey = getSubFieldOptionKey(
|
||||
selectedFieldMetadataItem,
|
||||
subFieldNameSelected,
|
||||
);
|
||||
|
||||
return option.value === optionKey;
|
||||
});
|
||||
|
||||
if (isDefined(correspondingOption)) {
|
||||
@@ -108,9 +112,9 @@ export const MatchColumnToFieldSelect = ({
|
||||
closeDropdown(dropdownId);
|
||||
};
|
||||
|
||||
const shouldShowNestedField =
|
||||
const shouldShowSubField =
|
||||
isDefined(selectedFieldMetadataItem) &&
|
||||
hasNestedFields(selectedFieldMetadataItem);
|
||||
isCompositeFieldType(selectedFieldMetadataItem.type);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
@@ -125,7 +129,7 @@ export const MatchColumnToFieldSelect = ({
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
shouldShowNestedField ? (
|
||||
shouldShowSubField ? (
|
||||
<MatchColumnSelectSubFieldSelectDropdownContent
|
||||
fieldMetadataItem={selectedFieldMetadataItem}
|
||||
onSubFieldSelect={handleSubFieldSelect}
|
||||
|
||||
+5
-5
@@ -5,16 +5,16 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export const RsiContext = createContext({} as any);
|
||||
|
||||
type ReactSpreadsheetImportContextProviderProps = {
|
||||
type ReactSpreadsheetImportContextProviderProps<T extends string> = {
|
||||
children: React.ReactNode;
|
||||
values: SpreadsheetImportDialogOptions;
|
||||
values: SpreadsheetImportDialogOptions<T>;
|
||||
};
|
||||
|
||||
export const ReactSpreadsheetImportContextProvider = ({
|
||||
export const ReactSpreadsheetImportContextProvider = <T extends string>({
|
||||
children,
|
||||
values,
|
||||
}: ReactSpreadsheetImportContextProviderProps) => {
|
||||
if (isUndefinedOrNull(values.spreadsheetImportFields)) {
|
||||
}: ReactSpreadsheetImportContextProviderProps<T>) => {
|
||||
if (isUndefinedOrNull(values.fields)) {
|
||||
throw new Error('Fields must be provided to spreadsheet-import');
|
||||
}
|
||||
|
||||
|
||||
+33
-31
@@ -13,43 +13,45 @@ import { act } from 'react';
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<RecoilRoot>{children}</RecoilRoot>
|
||||
);
|
||||
type SpreadsheetKey = 'spreadsheet_key';
|
||||
|
||||
export const mockedSpreadsheetOptions: SpreadsheetImportDialogOptions = {
|
||||
onClose: () => {},
|
||||
spreadsheetImportFields: [],
|
||||
uploadStepHook: async () => [],
|
||||
selectHeaderStepHook: async (
|
||||
headerValues: ImportedRow,
|
||||
data: ImportedRow[],
|
||||
) => ({
|
||||
headerRow: headerValues,
|
||||
importedRows: data,
|
||||
}),
|
||||
matchColumnsStepHook: async () => [],
|
||||
rowHook: () => ({ spreadsheet_key: 'rowHook' }),
|
||||
tableHook: () => [{ spreadsheet_key: 'tableHook' }],
|
||||
onSubmit: async () => {},
|
||||
allowInvalidSubmit: false,
|
||||
customTheme: {},
|
||||
maxRecords: 10,
|
||||
maxFileSize: 50,
|
||||
autoMapHeaders: true,
|
||||
autoMapDistance: 1,
|
||||
initialStepState: {
|
||||
type: SpreadsheetImportStepType.upload,
|
||||
},
|
||||
dateFormat: 'MM/DD/YY',
|
||||
parseRaw: true,
|
||||
rtl: false,
|
||||
selectHeader: true,
|
||||
availableFieldMetadataItems: [],
|
||||
};
|
||||
export const mockedSpreadsheetOptions: SpreadsheetImportDialogOptions<SpreadsheetKey> =
|
||||
{
|
||||
onClose: () => {},
|
||||
fields: [],
|
||||
uploadStepHook: async () => [],
|
||||
selectHeaderStepHook: async (
|
||||
headerValues: ImportedRow,
|
||||
data: ImportedRow[],
|
||||
) => ({
|
||||
headerRow: headerValues,
|
||||
importedRows: data,
|
||||
}),
|
||||
matchColumnsStepHook: async () => [],
|
||||
rowHook: () => ({ spreadsheet_key: 'rowHook' }),
|
||||
tableHook: () => [{ spreadsheet_key: 'tableHook' }],
|
||||
onSubmit: async () => {},
|
||||
allowInvalidSubmit: false,
|
||||
customTheme: {},
|
||||
maxRecords: 10,
|
||||
maxFileSize: 50,
|
||||
autoMapHeaders: true,
|
||||
autoMapDistance: 1,
|
||||
initialStepState: {
|
||||
type: SpreadsheetImportStepType.upload,
|
||||
},
|
||||
dateFormat: 'MM/DD/YY',
|
||||
parseRaw: true,
|
||||
rtl: false,
|
||||
selectHeader: true,
|
||||
availableFieldMetadataItems: [],
|
||||
};
|
||||
|
||||
describe('useSpreadsheetImport', () => {
|
||||
it('should set isOpen to true, and update the options in the Recoil state', async () => {
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
useSpreadsheetImport: useOpenSpreadsheetImportDialog(),
|
||||
useSpreadsheetImport: useOpenSpreadsheetImportDialog<SpreadsheetKey>(),
|
||||
spreadsheetImportState: useRecoilState(spreadsheetImportDialogState)[0],
|
||||
}),
|
||||
{
|
||||
|
||||
+2
-3
@@ -8,9 +8,8 @@ import { ImportedRow } from '@/spreadsheet-import/types';
|
||||
import { getMatchedColumnsWithFuse } from '@/spreadsheet-import/utils/getMatchedColumnsWithFuse';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useComputeColumnSuggestionsAndAutoMatch = () => {
|
||||
const { spreadsheetImportFields: fields, autoMapHeaders } =
|
||||
useSpreadsheetImportInternal();
|
||||
export const useComputeColumnSuggestionsAndAutoMatch = <T extends string>() => {
|
||||
const { fields, autoMapHeaders } = useSpreadsheetImportInternal<T>();
|
||||
|
||||
const computeColumnSuggestionsAndAutoMatch = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
|
||||
+2
-2
@@ -4,13 +4,13 @@ import { SPREADSHEET_IMPORT_MODAL_ID } from '@/spreadsheet-import/constants/Spre
|
||||
import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState';
|
||||
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
export const useOpenSpreadsheetImportDialog = () => {
|
||||
export const useOpenSpreadsheetImportDialog = <T extends string>() => {
|
||||
const setSpreadSheetImport = useSetRecoilState(spreadsheetImportDialogState);
|
||||
|
||||
const { openModal } = useModal();
|
||||
|
||||
const openSpreadsheetImportDialog = (
|
||||
options: Omit<SpreadsheetImportDialogOptions, 'isOpen' | 'onClose'>,
|
||||
options: Omit<SpreadsheetImportDialogOptions<T>, 'isOpen' | 'onClose'>,
|
||||
) => {
|
||||
openModal(SPREADSHEET_IMPORT_MODAL_ID);
|
||||
setSpreadSheetImport({
|
||||
|
||||
+2
-2
@@ -5,10 +5,10 @@ import { RsiContext } from '@/spreadsheet-import/components/ReactSpreadsheetImpo
|
||||
import { defaultSpreadsheetImportProps } from '@/spreadsheet-import/provider/components/SpreadsheetImport';
|
||||
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
|
||||
|
||||
export const useSpreadsheetImportInternal = () =>
|
||||
export const useSpreadsheetImportInternal = <T extends string>() =>
|
||||
useContext<
|
||||
SetRequired<
|
||||
SpreadsheetImportDialogOptions,
|
||||
SpreadsheetImportDialogOptions<T>,
|
||||
keyof typeof defaultSpreadsheetImportProps
|
||||
>
|
||||
>(RsiContext);
|
||||
|
||||
+7
-3
@@ -10,7 +10,9 @@ import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogMa
|
||||
import { useStepBar } from '@/ui/navigation/step-bar/hooks/useStepBar';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
export const defaultSpreadsheetImportProps: Partial<SpreadsheetImportProps> = {
|
||||
export const defaultSpreadsheetImportProps: Partial<
|
||||
SpreadsheetImportProps<any>
|
||||
> = {
|
||||
autoMapHeaders: true,
|
||||
allowInvalidSubmit: true,
|
||||
autoMapDistance: 2,
|
||||
@@ -26,11 +28,13 @@ export const defaultSpreadsheetImportProps: Partial<SpreadsheetImportProps> = {
|
||||
maxRecords: SpreadsheetMaxRecordImportCapacity,
|
||||
} as const;
|
||||
|
||||
export const SpreadsheetImport = (props: SpreadsheetImportProps) => {
|
||||
export const SpreadsheetImport = <T extends string>(
|
||||
props: SpreadsheetImportProps<T>,
|
||||
) => {
|
||||
const mergedProps = {
|
||||
...defaultSpreadsheetImportProps,
|
||||
...props,
|
||||
} as SpreadsheetImportProps;
|
||||
} as SpreadsheetImportProps<T>;
|
||||
|
||||
const { enqueueDialog } = useDialogManager();
|
||||
|
||||
|
||||
+12
-11
@@ -1,18 +1,19 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
import { SpreadsheetImportDialogOptions } from '../types';
|
||||
|
||||
export type SpreadsheetImportDialogState = {
|
||||
export type SpreadsheetImportDialogState<T extends string> = {
|
||||
isOpen: boolean;
|
||||
isStepBarVisible: boolean;
|
||||
options: Omit<SpreadsheetImportDialogOptions, 'isOpen' | 'onClose'> | null;
|
||||
options: Omit<SpreadsheetImportDialogOptions<T>, 'isOpen' | 'onClose'> | null;
|
||||
};
|
||||
|
||||
export const spreadsheetImportDialogState =
|
||||
createState<SpreadsheetImportDialogState>({
|
||||
key: 'spreadsheetImportDialogState',
|
||||
defaultValue: {
|
||||
isOpen: false,
|
||||
isStepBarVisible: true,
|
||||
options: null,
|
||||
},
|
||||
});
|
||||
export const spreadsheetImportDialogState = createState<
|
||||
SpreadsheetImportDialogState<any>
|
||||
>({
|
||||
key: 'spreadsheetImportDialogState',
|
||||
defaultValue: {
|
||||
isOpen: false,
|
||||
isStepBarVisible: true,
|
||||
options: null,
|
||||
},
|
||||
});
|
||||
|
||||
+4
@@ -1,6 +1,7 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton';
|
||||
import { useHideStepBar } from '@/spreadsheet-import/hooks/useHideStepBar';
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import { spreadsheetImportCreatedRecordsProgressState } from '@/spreadsheet-import/states/spreadsheetImportCreatedRecordsProgressState';
|
||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||
@@ -37,6 +38,9 @@ type ImportDataStepProps = {
|
||||
export const ImportDataStep = ({
|
||||
recordsToImportCount,
|
||||
}: ImportDataStepProps) => {
|
||||
const hideStepBar = useHideStepBar();
|
||||
hideStepBar();
|
||||
|
||||
const { onClose } = useSpreadsheetImportInternal();
|
||||
const spreadsheetImportCreatedRecordsProgress = useRecoilValue(
|
||||
spreadsheetImportCreatedRecordsProgressState,
|
||||
|
||||
+8
-8
@@ -64,7 +64,7 @@ export type MatchColumnsStepProps = {
|
||||
onError: (message: string) => void;
|
||||
};
|
||||
|
||||
export const MatchColumnsStep = ({
|
||||
export const MatchColumnsStep = <T extends string>({
|
||||
data,
|
||||
headerValues,
|
||||
onBack,
|
||||
@@ -76,7 +76,7 @@ export const MatchColumnsStep = ({
|
||||
}: MatchColumnsStepProps) => {
|
||||
const { enqueueDialog } = useDialogManager();
|
||||
const dataExample = data.slice(0, 2);
|
||||
const { spreadsheetImportFields: fields } = useSpreadsheetImportInternal();
|
||||
const { fields } = useSpreadsheetImportInternal<T>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [columns, setColumns] = useRecoilState(
|
||||
initialComputedColumnsSelector(headerValues),
|
||||
@@ -90,7 +90,7 @@ export const MatchColumnsStep = ({
|
||||
(columnIndex: number) => {
|
||||
setColumns(
|
||||
columns.map((column, index) =>
|
||||
columnIndex === index ? setIgnoreColumn(column) : column,
|
||||
columnIndex === index ? setIgnoreColumn<string>(column) : column,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -109,7 +109,7 @@ export const MatchColumnsStep = ({
|
||||
);
|
||||
|
||||
const onChange = useCallback(
|
||||
(value: string, columnIndex: number) => {
|
||||
(value: T, columnIndex: number) => {
|
||||
if (value === DO_NOT_IMPORT_OPTION_KEY) {
|
||||
if (columns[columnIndex].type === SpreadsheetColumnType.ignored) {
|
||||
onRevertIgnore(columnIndex);
|
||||
@@ -119,12 +119,12 @@ export const MatchColumnsStep = ({
|
||||
} else {
|
||||
const field = fields.find(
|
||||
(field) => field.key === value,
|
||||
) as unknown as SpreadsheetImportField;
|
||||
) as unknown as SpreadsheetImportField<T>;
|
||||
const existingFieldIndex = columns.findIndex(
|
||||
(column) => 'value' in column && column.value === field.key,
|
||||
);
|
||||
setColumns(
|
||||
columns.map<SpreadsheetColumn>((column, index) => {
|
||||
columns.map<SpreadsheetColumn<string>>((column, index) => {
|
||||
if (columnIndex === index) {
|
||||
return setColumn(column, field, data);
|
||||
} else if (index === existingFieldIndex) {
|
||||
@@ -141,9 +141,9 @@ export const MatchColumnsStep = ({
|
||||
|
||||
const handleContinue = useCallback(
|
||||
async (
|
||||
values: ImportedStructuredRow[],
|
||||
values: ImportedStructuredRow<string>[],
|
||||
rawData: ImportedRow[],
|
||||
columns: SpreadsheetColumns,
|
||||
columns: SpreadsheetColumns<string>,
|
||||
) => {
|
||||
try {
|
||||
const data = await matchColumnsStepHook(values, rawData, columns);
|
||||
|
||||
+7
-7
@@ -82,28 +82,28 @@ const StyledGridHeader = styled.div<PositionProps>`
|
||||
padding-right: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
type ColumnGridProps = {
|
||||
columns: SpreadsheetColumns;
|
||||
type ColumnGridProps<T extends string> = {
|
||||
columns: SpreadsheetColumns<T>;
|
||||
renderUserColumn: (
|
||||
columns: SpreadsheetColumns,
|
||||
columns: SpreadsheetColumns<T>,
|
||||
columnIndex: number,
|
||||
) => React.ReactNode;
|
||||
renderTemplateColumn: (
|
||||
columns: SpreadsheetColumns,
|
||||
columns: SpreadsheetColumns<T>,
|
||||
columnIndex: number,
|
||||
) => React.ReactNode;
|
||||
renderUnmatchedColumn: (
|
||||
columns: SpreadsheetColumns,
|
||||
columns: SpreadsheetColumns<T>,
|
||||
columnIndex: number,
|
||||
) => React.ReactNode;
|
||||
};
|
||||
|
||||
export const ColumnGrid = ({
|
||||
export const ColumnGrid = <T extends string>({
|
||||
columns,
|
||||
renderUserColumn,
|
||||
renderTemplateColumn,
|
||||
renderUnmatchedColumn,
|
||||
}: ColumnGridProps) => {
|
||||
}: ColumnGridProps<T>) => {
|
||||
return (
|
||||
<>
|
||||
<StyledGridContainer>
|
||||
|
||||
+7
-7
@@ -16,20 +16,20 @@ const StyledIconChevronDown = styled(IconChevronDown)`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
export type SubMatchingSelectDropdownButtonProps = {
|
||||
option: SpreadsheetMatchedOptions | Partial<SpreadsheetMatchedOptions>;
|
||||
export type SubMatchingSelectDropdownButtonProps<T> = {
|
||||
option: SpreadsheetMatchedOptions<T> | Partial<SpreadsheetMatchedOptions<T>>;
|
||||
column:
|
||||
| SpreadsheetMatchedSelectColumn
|
||||
| SpreadsheetMatchedSelectOptionsColumn;
|
||||
| SpreadsheetMatchedSelectColumn<T>
|
||||
| SpreadsheetMatchedSelectOptionsColumn<T>;
|
||||
placeholder: string;
|
||||
};
|
||||
|
||||
export const SubMatchingSelectDropdownButton = ({
|
||||
export const SubMatchingSelectDropdownButton = <T extends string>({
|
||||
option,
|
||||
column,
|
||||
placeholder,
|
||||
}: SubMatchingSelectDropdownButtonProps) => {
|
||||
const { spreadsheetImportFields: fields } = useSpreadsheetImportInternal();
|
||||
}: SubMatchingSelectDropdownButtonProps<T>) => {
|
||||
const { fields } = useSpreadsheetImportInternal<T>();
|
||||
const options = getFieldOptions(fields, column.value) as SelectOption[];
|
||||
const value = options.find((opt) => opt.value === option.value);
|
||||
|
||||
|
||||
+9
-9
@@ -15,23 +15,23 @@ const StyledRowContainer = styled.div`
|
||||
padding-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
interface SubMatchingSelectRowProps {
|
||||
option: SpreadsheetMatchedOptions | Partial<SpreadsheetMatchedOptions>;
|
||||
interface SubMatchingSelectRowProps<T> {
|
||||
option: SpreadsheetMatchedOptions<T> | Partial<SpreadsheetMatchedOptions<T>>;
|
||||
column:
|
||||
| SpreadsheetMatchedSelectColumn
|
||||
| SpreadsheetMatchedSelectOptionsColumn;
|
||||
onSubChange: (val: string, index: number, option: string) => void;
|
||||
| SpreadsheetMatchedSelectColumn<T>
|
||||
| SpreadsheetMatchedSelectOptionsColumn<T>;
|
||||
onSubChange: (val: T, index: number, option: string) => void;
|
||||
placeholder: string;
|
||||
selectedOption?:
|
||||
| SpreadsheetMatchedOptions
|
||||
| Partial<SpreadsheetMatchedOptions>;
|
||||
| SpreadsheetMatchedOptions<T>
|
||||
| Partial<SpreadsheetMatchedOptions<T>>;
|
||||
}
|
||||
export const SubMatchingSelectRow = ({
|
||||
export const SubMatchingSelectRow = <T extends string>({
|
||||
option,
|
||||
column,
|
||||
onSubChange,
|
||||
placeholder,
|
||||
}: SubMatchingSelectRowProps) => {
|
||||
}: SubMatchingSelectRowProps<T>) => {
|
||||
return (
|
||||
<StyledRowContainer>
|
||||
<SubMatchingSelectRowLeftSelect option={option} />
|
||||
|
||||
+4
-4
@@ -15,13 +15,13 @@ const StyledControlLabel = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export type SubMatchingSelectRowLeftSelectProps = {
|
||||
option: SpreadsheetMatchedOptions | Partial<SpreadsheetMatchedOptions>;
|
||||
export type SubMatchingSelectRowLeftSelectProps<T> = {
|
||||
option: SpreadsheetMatchedOptions<T> | Partial<SpreadsheetMatchedOptions<T>>;
|
||||
};
|
||||
|
||||
export const SubMatchingSelectRowLeftSelect = ({
|
||||
export const SubMatchingSelectRowLeftSelect = <T extends string>({
|
||||
option,
|
||||
}: SubMatchingSelectRowLeftSelectProps) => {
|
||||
}: SubMatchingSelectRowLeftSelectProps<T>) => {
|
||||
return (
|
||||
<SubMatchingSelectControlContainer cursor="default">
|
||||
<StyledControlLabel>
|
||||
|
||||
+11
-11
@@ -18,34 +18,34 @@ const StyledDropdownContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
interface SubMatchingSelectRowRightDropdownProps {
|
||||
option: SpreadsheetMatchedOptions | Partial<SpreadsheetMatchedOptions>;
|
||||
interface SubMatchingSelectRowRightDropdownProps<T> {
|
||||
option: SpreadsheetMatchedOptions<T> | Partial<SpreadsheetMatchedOptions<T>>;
|
||||
column:
|
||||
| SpreadsheetMatchedSelectColumn
|
||||
| SpreadsheetMatchedSelectOptionsColumn;
|
||||
onSubChange: (val: string, index: number, option: string) => void;
|
||||
| SpreadsheetMatchedSelectColumn<T>
|
||||
| SpreadsheetMatchedSelectOptionsColumn<T>;
|
||||
onSubChange: (val: T, index: number, option: string) => void;
|
||||
placeholder: string;
|
||||
selectedOption?:
|
||||
| SpreadsheetMatchedOptions
|
||||
| Partial<SpreadsheetMatchedOptions>;
|
||||
| SpreadsheetMatchedOptions<T>
|
||||
| Partial<SpreadsheetMatchedOptions<T>>;
|
||||
}
|
||||
|
||||
export const SubMatchingSelectRowRightDropdown = ({
|
||||
export const SubMatchingSelectRowRightDropdown = <T extends string>({
|
||||
option,
|
||||
column,
|
||||
onSubChange,
|
||||
placeholder,
|
||||
}: SubMatchingSelectRowRightDropdownProps) => {
|
||||
}: SubMatchingSelectRowRightDropdownProps<T>) => {
|
||||
const dropdownId = `sub-matching-select-dropdown-${option.entry}`;
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const { spreadsheetImportFields: fields } = useSpreadsheetImportInternal();
|
||||
const { fields } = useSpreadsheetImportInternal<T>();
|
||||
const options = getFieldOptions(fields, column.value) as SelectOption[];
|
||||
const value = options.find((opt) => opt.value === option.value);
|
||||
|
||||
const handleSelect = (selectedOption: SelectOption) => {
|
||||
onSubChange(selectedOption.value, column.index, option.entry ?? '');
|
||||
onSubChange(selectedOption.value as T, column.index, option.entry ?? '');
|
||||
closeDropdown(dropdownId);
|
||||
};
|
||||
|
||||
|
||||
+10
-10
@@ -6,7 +6,7 @@ import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpre
|
||||
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 { spreadsheetImportBuildFieldOptions } from '@/spreadsheet-import/utils/spreadsheetImportBuildFieldOptions';
|
||||
import { spreadsheetBuildFieldOptions } from '@/spreadsheet-import/utils/spreadsheetBuildFieldOptions';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconForbid } from 'twenty-ui/display';
|
||||
@@ -25,18 +25,18 @@ const StyledErrorMessage = styled.span`
|
||||
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
type TemplateColumnProps = {
|
||||
columns: SpreadsheetColumns;
|
||||
type TemplateColumnProps<T extends string> = {
|
||||
columns: SpreadsheetColumns<string>;
|
||||
columnIndex: number;
|
||||
onChange: (val: string, index: number) => void;
|
||||
onChange: (val: T, index: number) => void;
|
||||
};
|
||||
|
||||
export const TemplateColumn = ({
|
||||
export const TemplateColumn = <T extends string>({
|
||||
columns,
|
||||
columnIndex,
|
||||
onChange,
|
||||
}: TemplateColumnProps) => {
|
||||
const { spreadsheetImportFields: fields } = useSpreadsheetImportInternal();
|
||||
}: TemplateColumnProps<T>) => {
|
||||
const { fields } = useSpreadsheetImportInternal<T>();
|
||||
const suggestedFieldsByColumnHeader = useRecoilValue(
|
||||
suggestedFieldsByColumnHeaderState,
|
||||
);
|
||||
@@ -46,8 +46,8 @@ export const TemplateColumn = ({
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
const fieldOptions = spreadsheetImportBuildFieldOptions(fields, columns);
|
||||
const suggestedFieldOptions = spreadsheetImportBuildFieldOptions(
|
||||
const fieldOptions = spreadsheetBuildFieldOptions(fields, columns);
|
||||
const suggestedFieldOptions = spreadsheetBuildFieldOptions(
|
||||
suggestedFieldsByColumnHeader[column.header] ?? [],
|
||||
columns,
|
||||
);
|
||||
@@ -74,7 +74,7 @@ export const TemplateColumn = ({
|
||||
<MatchColumnToFieldSelect
|
||||
placeholder={t`Select column...`}
|
||||
value={isIgnored ? ignoreValue : selectValue}
|
||||
onChange={(value) => onChange(value?.value as string, column.index)}
|
||||
onChange={(value) => onChange(value?.value as T, column.index)}
|
||||
options={selectOptions}
|
||||
suggestedOptions={suggestedFieldOptions}
|
||||
columnIndex={column.index.toString()}
|
||||
|
||||
+9
-9
@@ -11,9 +11,9 @@ import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
|
||||
const getExpandableContainerTitle = (
|
||||
fields: SpreadsheetImportFields,
|
||||
column: SpreadsheetColumn,
|
||||
const getExpandableContainerTitle = <T extends string>(
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
column: SpreadsheetColumn<T>,
|
||||
) => {
|
||||
const fieldLabel = fields.find(
|
||||
(field) => 'value' in column && field.key === column.value,
|
||||
@@ -25,10 +25,10 @@ const getExpandableContainerTitle = (
|
||||
} Unmatched)`;
|
||||
};
|
||||
|
||||
type UnmatchColumnProps = {
|
||||
columns: SpreadsheetColumns;
|
||||
type UnmatchColumnProps<T extends string> = {
|
||||
columns: SpreadsheetColumns<T>;
|
||||
columnIndex: number;
|
||||
onSubChange: (val: string, index: number, option: string) => void;
|
||||
onSubChange: (val: T, index: number, option: string) => void;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@@ -44,12 +44,12 @@ const StyledContentWrapper = styled.div`
|
||||
padding-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export const UnmatchColumn = ({
|
||||
export const UnmatchColumn = <T extends string>({
|
||||
columns,
|
||||
columnIndex,
|
||||
onSubChange,
|
||||
}: UnmatchColumnProps) => {
|
||||
const { spreadsheetImportFields: fields } = useSpreadsheetImportInternal();
|
||||
}: UnmatchColumnProps<T>) => {
|
||||
const { fields } = useSpreadsheetImportInternal<T>();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const column = columns[columnIndex];
|
||||
const isSelect = 'matchedOptions' in column;
|
||||
|
||||
+4
-4
@@ -29,15 +29,15 @@ const StyledExample = styled.span`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
type UserTableColumnProps = {
|
||||
column: SpreadsheetColumn;
|
||||
type UserTableColumnProps<T extends string> = {
|
||||
column: SpreadsheetColumn<T>;
|
||||
importedRow: ImportedRow;
|
||||
};
|
||||
|
||||
export const UserTableColumn = ({
|
||||
export const UserTableColumn = <T extends string>({
|
||||
column,
|
||||
importedRow,
|
||||
}: UserTableColumnProps) => {
|
||||
}: UserTableColumnProps<T>) => {
|
||||
const { header } = column;
|
||||
const firstDefinedValue = importedRow.find(isDefined);
|
||||
|
||||
|
||||
+5
-5
@@ -5,18 +5,18 @@ import { atom, selectorFamily } from 'recoil';
|
||||
|
||||
export const matchColumnsState = atom({
|
||||
key: 'MatchColumnsState',
|
||||
default: [] as SpreadsheetColumns,
|
||||
default: [] as SpreadsheetColumns<string>,
|
||||
});
|
||||
|
||||
export const initialComputedColumnsSelector = selectorFamily<
|
||||
SpreadsheetColumns,
|
||||
SpreadsheetColumns<string>,
|
||||
ImportedRow
|
||||
>({
|
||||
key: 'initialComputedColumnsSelector',
|
||||
get:
|
||||
(headerValues: ImportedRow) =>
|
||||
({ get }) => {
|
||||
const currentState = get(matchColumnsState) as SpreadsheetColumns;
|
||||
const currentState = get(matchColumnsState) as SpreadsheetColumns<string>;
|
||||
if (currentState.length === 0) {
|
||||
// Do not remove spread, it indexes empty array elements, otherwise map() skips over them
|
||||
const initialState = ([...headerValues] as string[]).map(
|
||||
@@ -26,7 +26,7 @@ export const initialComputedColumnsSelector = selectorFamily<
|
||||
header: value ?? '',
|
||||
}),
|
||||
);
|
||||
return initialState as SpreadsheetColumns;
|
||||
return initialState as SpreadsheetColumns<string>;
|
||||
} else {
|
||||
return currentState;
|
||||
}
|
||||
@@ -34,6 +34,6 @@ export const initialComputedColumnsSelector = selectorFamily<
|
||||
set:
|
||||
() =>
|
||||
({ set }, newValue) => {
|
||||
set(matchColumnsState, newValue as SpreadsheetColumns);
|
||||
set(matchColumnsState, newValue as SpreadsheetColumns<string>);
|
||||
},
|
||||
});
|
||||
|
||||
+1
-1
@@ -3,5 +3,5 @@ import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export const suggestedFieldsByColumnHeaderState = createState({
|
||||
key: 'suggestedFieldsByColumnHeaderState',
|
||||
defaultValue: {} as Record<string, SpreadsheetImportField[]>,
|
||||
defaultValue: {} as Record<string, SpreadsheetImportField<string>[]>,
|
||||
});
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import styled from '@emotion/styled';
|
||||
// @ts-expect-error // Todo: remove usage of react-data-grid
|
||||
import { Column } from 'react-data-grid';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||
import { AppTooltip } from 'twenty-ui/display';
|
||||
|
||||
const StyledHeaderContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const StyledHeaderLabel = styled.span`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const StyledDefaultContainer = styled.div`
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
export const generateColumns = <T extends string>(
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
) =>
|
||||
fields.map(
|
||||
(column): Column<any> => ({
|
||||
key: column.key,
|
||||
name: column.label,
|
||||
minWidth: 150,
|
||||
headerRenderer: () => (
|
||||
<StyledHeaderContainer>
|
||||
<StyledHeaderLabel id={`${column.key}`}>
|
||||
{column.label}
|
||||
</StyledHeaderLabel>
|
||||
{column.description &&
|
||||
createPortal(
|
||||
<AppTooltip
|
||||
anchorSelect={`#${column.key}`}
|
||||
place="top"
|
||||
content={column.description}
|
||||
/>,
|
||||
document.body,
|
||||
)}
|
||||
</StyledHeaderContainer>
|
||||
),
|
||||
formatter: ({ row }: any) => (
|
||||
<StyledDefaultContainer>{row[column.key]}</StyledDefaultContainer>
|
||||
),
|
||||
}),
|
||||
);
|
||||
+3
-4
@@ -1,6 +1,5 @@
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { spreadsheetImportFilterAvailableFieldMetadataItems } from '@/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems';
|
||||
import { getCompositeSubFieldLabelWithFieldLabel } from '@/object-record/spreadsheet-import/utils/spreadsheetImportGetCompositeSubFieldLabelWithFieldLabel';
|
||||
import { spreadsheetImportFilterAvailableFieldMetadataItems } from '@/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts';
|
||||
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 { escapeCSVValue } from '@/spreadsheet-import/utils/escapeCSVValue';
|
||||
@@ -59,8 +58,8 @@ export const useDownloadFakeRecords = () => {
|
||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[field.type].exampleValues;
|
||||
|
||||
headerRow.push(
|
||||
...subFields.map(({ subFieldLabel }) =>
|
||||
getCompositeSubFieldLabelWithFieldLabel(field, subFieldLabel),
|
||||
...subFields.map(
|
||||
({ subFieldLabel }) => `${field.label} / ${subFieldLabel}`,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
+17
-24
@@ -1,6 +1,5 @@
|
||||
import { SpreadsheetImportTable } from '@/spreadsheet-import/components/SpreadsheetImportTable';
|
||||
import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton';
|
||||
import { useHideStepBar } from '@/spreadsheet-import/hooks/useHideStepBar';
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep';
|
||||
import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType';
|
||||
@@ -92,36 +91,30 @@ const StyledNoRowsWithErrorsContainer = styled.div`
|
||||
margin: auto 0;
|
||||
`;
|
||||
|
||||
type ValidationStepProps = {
|
||||
initialData: ImportedStructuredRow[];
|
||||
importedColumns: SpreadsheetColumns;
|
||||
type ValidationStepProps<T extends string> = {
|
||||
initialData: ImportedStructuredRow<T>[];
|
||||
importedColumns: SpreadsheetColumns<string>;
|
||||
file: File;
|
||||
onBack: () => void;
|
||||
setCurrentStepState: Dispatch<SetStateAction<SpreadsheetImportStep>>;
|
||||
};
|
||||
|
||||
export const ValidationStep = ({
|
||||
export const ValidationStep = <T extends string>({
|
||||
initialData,
|
||||
importedColumns,
|
||||
file,
|
||||
setCurrentStepState,
|
||||
onBack,
|
||||
}: ValidationStepProps) => {
|
||||
const hideStepBar = useHideStepBar();
|
||||
}: ValidationStepProps<T>) => {
|
||||
const { enqueueDialog } = useDialogManager();
|
||||
const {
|
||||
spreadsheetImportFields: fields,
|
||||
onClose,
|
||||
onSubmit,
|
||||
rowHook,
|
||||
tableHook,
|
||||
} = useSpreadsheetImportInternal();
|
||||
const { fields, onClose, onSubmit, rowHook, tableHook } =
|
||||
useSpreadsheetImportInternal<T>();
|
||||
|
||||
const [data, setData] = useState<
|
||||
(ImportedStructuredRow & ImportedStructuredRowMetadata)[]
|
||||
(ImportedStructuredRow<T> & ImportedStructuredRowMetadata)[]
|
||||
>(
|
||||
useMemo(
|
||||
() => addErrorsAndRunHooks(initialData, fields, rowHook, tableHook),
|
||||
() => addErrorsAndRunHooks<T>(initialData, fields, rowHook, tableHook),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
),
|
||||
@@ -133,7 +126,7 @@ export const ValidationStep = ({
|
||||
|
||||
const updateData = useCallback(
|
||||
(rows: typeof data) => {
|
||||
setData(addErrorsAndRunHooks(rows, fields, rowHook, tableHook));
|
||||
setData(addErrorsAndRunHooks<T>(rows, fields, rowHook, tableHook));
|
||||
},
|
||||
[setData, rowHook, tableHook, fields],
|
||||
);
|
||||
@@ -212,7 +205,8 @@ export const ValidationStep = ({
|
||||
}, [data, filterByErrors]);
|
||||
|
||||
const rowKeyGetter = useCallback(
|
||||
(row: ImportedStructuredRow & ImportedStructuredRowMetadata) => row.__index,
|
||||
(row: ImportedStructuredRow<T> & ImportedStructuredRowMetadata) =>
|
||||
row.__index,
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -224,29 +218,28 @@ export const ValidationStep = ({
|
||||
for (const key in __errors) {
|
||||
if (__errors[key].level === 'error') {
|
||||
acc.invalidStructuredRows.push(
|
||||
values as unknown as ImportedStructuredRow,
|
||||
values as unknown as ImportedStructuredRow<T>,
|
||||
);
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
}
|
||||
acc.validStructuredRows.push(
|
||||
values as unknown as ImportedStructuredRow,
|
||||
values as unknown as ImportedStructuredRow<T>,
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
validStructuredRows: [] as ImportedStructuredRow[],
|
||||
invalidStructuredRows: [] as ImportedStructuredRow[],
|
||||
validStructuredRows: [] as ImportedStructuredRow<T>[],
|
||||
invalidStructuredRows: [] as ImportedStructuredRow<T>[],
|
||||
allStructuredRows: data,
|
||||
} satisfies SpreadsheetImportImportValidationResult,
|
||||
} satisfies SpreadsheetImportImportValidationResult<T>,
|
||||
);
|
||||
|
||||
setCurrentStepState({
|
||||
type: SpreadsheetImportStepType.importData,
|
||||
recordsToImportCount: calculatedData.validStructuredRows.length,
|
||||
});
|
||||
hideStepBar();
|
||||
|
||||
await onSubmit(calculatedData, file);
|
||||
onClose();
|
||||
|
||||
+7
-7
@@ -71,9 +71,9 @@ const formatSafeId = (columnKey: string) => {
|
||||
return camelCase(columnKey.replace('(', '').replace(')', ''));
|
||||
};
|
||||
|
||||
export const generateColumns = (
|
||||
fields: SpreadsheetImportFields,
|
||||
): Column<ImportedStructuredRow & ImportedStructuredRowMetadata>[] => [
|
||||
export const generateColumns = <T extends string>(
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
): Column<ImportedStructuredRow<T> & ImportedStructuredRowMetadata>[] => [
|
||||
{
|
||||
key: SELECT_COLUMN_KEY,
|
||||
name: '',
|
||||
@@ -108,7 +108,7 @@ export const generateColumns = (
|
||||
...fields.map(
|
||||
(
|
||||
column,
|
||||
): Column<ImportedStructuredRow & ImportedStructuredRowMetadata> => ({
|
||||
): Column<ImportedStructuredRow<T> & ImportedStructuredRowMetadata> => ({
|
||||
key: column.key,
|
||||
name: column.label,
|
||||
minWidth: 150,
|
||||
@@ -132,7 +132,7 @@ export const generateColumns = (
|
||||
editable: column.fieldType.type !== 'checkbox',
|
||||
// Todo: remove usage of react-data-grid
|
||||
editor: ({ row, onRowChange, onClose }: any) => {
|
||||
const columnKey = column.key as keyof (ImportedStructuredRow &
|
||||
const columnKey = column.key as keyof (ImportedStructuredRow<T> &
|
||||
ImportedStructuredRowMetadata);
|
||||
let component;
|
||||
|
||||
@@ -166,7 +166,7 @@ export const generateColumns = (
|
||||
},
|
||||
// Todo: remove usage of react-data-grid
|
||||
formatter: ({ row, onRowChange }: { row: any; onRowChange: any }) => {
|
||||
const columnKey = column.key as keyof (ImportedStructuredRow &
|
||||
const columnKey = column.key as keyof (ImportedStructuredRow<T> &
|
||||
ImportedStructuredRowMetadata);
|
||||
let component;
|
||||
|
||||
@@ -197,7 +197,7 @@ export const generateColumns = (
|
||||
id={formatSafeId(`${columnKey}-${row.__index}`)}
|
||||
>
|
||||
{column.fieldType.options.find(
|
||||
(option) => option.value === row[columnKey],
|
||||
(option) => option.value === row[columnKey as T],
|
||||
)?.label || null}
|
||||
</StyledDefaultContainer>
|
||||
);
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ export type SpreadsheetImportStep =
|
||||
| {
|
||||
type: SpreadsheetImportStepType.validateData;
|
||||
data: any[];
|
||||
importedColumns: SpreadsheetColumns;
|
||||
importedColumns: SpreadsheetColumns<string>;
|
||||
}
|
||||
| {
|
||||
type: SpreadsheetImportStepType.loading;
|
||||
|
||||
@@ -13,49 +13,49 @@ type SpreadsheetIgnoredColumn = {
|
||||
header: string;
|
||||
};
|
||||
|
||||
type SpreadsheetMatchedColumn = {
|
||||
type SpreadsheetMatchedColumn<T> = {
|
||||
type: SpreadsheetColumnType.matched;
|
||||
index: number;
|
||||
header: string;
|
||||
value: string;
|
||||
value: T;
|
||||
};
|
||||
|
||||
type SpreadsheetMatchedSwitchColumn = {
|
||||
type SpreadsheetMatchedSwitchColumn<T> = {
|
||||
type: SpreadsheetColumnType.matchedCheckbox;
|
||||
index: number;
|
||||
header: string;
|
||||
value: string;
|
||||
value: T;
|
||||
};
|
||||
|
||||
export type SpreadsheetMatchedSelectColumn = {
|
||||
export type SpreadsheetMatchedSelectColumn<T> = {
|
||||
type: SpreadsheetColumnType.matchedSelect;
|
||||
index: number;
|
||||
header: string;
|
||||
value: string;
|
||||
matchedOptions: Partial<SpreadsheetMatchedOptions>[];
|
||||
value: T;
|
||||
matchedOptions: Partial<SpreadsheetMatchedOptions<T>>[];
|
||||
};
|
||||
|
||||
export type SpreadsheetMatchedSelectOptionsColumn = {
|
||||
export type SpreadsheetMatchedSelectOptionsColumn<T> = {
|
||||
type: SpreadsheetColumnType.matchedSelectOptions;
|
||||
index: number;
|
||||
header: string;
|
||||
value: string;
|
||||
matchedOptions: SpreadsheetMatchedOptions[];
|
||||
value: T;
|
||||
matchedOptions: SpreadsheetMatchedOptions<T>[];
|
||||
};
|
||||
|
||||
export type SpreadsheetErrorColumn = {
|
||||
export type SpreadsheetErrorColumn<T> = {
|
||||
type: SpreadsheetColumnType.matchedError;
|
||||
index: number;
|
||||
header: string;
|
||||
value: string;
|
||||
value: T;
|
||||
errorMessage: string;
|
||||
};
|
||||
|
||||
export type SpreadsheetColumn =
|
||||
export type SpreadsheetColumn<T extends string> =
|
||||
| SpreadsheetEmptyColumn
|
||||
| SpreadsheetIgnoredColumn
|
||||
| SpreadsheetMatchedColumn
|
||||
| SpreadsheetMatchedSwitchColumn
|
||||
| SpreadsheetMatchedSelectColumn
|
||||
| SpreadsheetMatchedSelectOptionsColumn
|
||||
| SpreadsheetErrorColumn;
|
||||
| SpreadsheetMatchedColumn<T>
|
||||
| SpreadsheetMatchedSwitchColumn<T>
|
||||
| SpreadsheetMatchedSelectColumn<T>
|
||||
| SpreadsheetMatchedSelectOptionsColumn<T>
|
||||
| SpreadsheetErrorColumn<T>;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||
|
||||
export type SpreadsheetColumns = SpreadsheetColumn[];
|
||||
export type SpreadsheetColumns<T extends string> = SpreadsheetColumn<T>[];
|
||||
|
||||
+8
-9
@@ -8,11 +8,11 @@ import { SpreadsheetImportRowHook } from '@/spreadsheet-import/types/Spreadsheet
|
||||
import { SpreadsheetImportTableHook } from '@/spreadsheet-import/types/SpreadsheetImportTableHook';
|
||||
import { SpreadsheetImportStep } from '../steps/types/SpreadsheetImportStep';
|
||||
|
||||
export type SpreadsheetImportDialogOptions = {
|
||||
export type SpreadsheetImportDialogOptions<FieldNames extends string> = {
|
||||
// callback when RSI is closed before final submit
|
||||
onClose: () => void;
|
||||
// Field description for requested data
|
||||
spreadsheetImportFields: SpreadsheetImportFields;
|
||||
fields: SpreadsheetImportFields<FieldNames>;
|
||||
// Runs after file upload step, receives and returns raw sheet data
|
||||
uploadStepHook?: (importedRows: ImportedRow[]) => Promise<ImportedRow[]>;
|
||||
// Runs after header selection step, receives and returns raw sheet data
|
||||
@@ -22,17 +22,17 @@ export type SpreadsheetImportDialogOptions = {
|
||||
) => Promise<{ headerRow: ImportedRow; importedRows: ImportedRow[] }>;
|
||||
// Runs once before validation step, used for data mutations and if you want to change how columns were matched
|
||||
matchColumnsStepHook?: (
|
||||
importedStructuredRows: ImportedStructuredRow[],
|
||||
importedStructuredRows: ImportedStructuredRow<FieldNames>[],
|
||||
importedRows: ImportedRow[],
|
||||
columns: SpreadsheetColumns,
|
||||
) => Promise<ImportedStructuredRow[]>;
|
||||
columns: SpreadsheetColumns<FieldNames>,
|
||||
) => Promise<ImportedStructuredRow<FieldNames>[]>;
|
||||
// Runs after column matching and on entry change
|
||||
rowHook?: SpreadsheetImportRowHook;
|
||||
rowHook?: SpreadsheetImportRowHook<FieldNames>;
|
||||
// Runs after column matching and on entry change
|
||||
tableHook?: SpreadsheetImportTableHook;
|
||||
tableHook?: SpreadsheetImportTableHook<FieldNames>;
|
||||
// Function called after user finishes the flow
|
||||
onSubmit: (
|
||||
validationResult: SpreadsheetImportImportValidationResult,
|
||||
validationResult: SpreadsheetImportImportValidationResult<FieldNames>,
|
||||
file: File,
|
||||
) => Promise<void>;
|
||||
// Function called when user aborts the importing flow
|
||||
@@ -59,6 +59,5 @@ export type SpreadsheetImportDialogOptions = {
|
||||
rtl?: boolean;
|
||||
// Allow header selection
|
||||
selectHeader?: boolean;
|
||||
// Available field for import
|
||||
availableFieldMetadataItems: FieldMetadataItem[];
|
||||
};
|
||||
|
||||
+6
-15
@@ -1,34 +1,25 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SpreadsheetImportFieldType } from '@/spreadsheet-import/types/SpreadsheetImportFieldType';
|
||||
import { SpreadsheetImportFieldValidationDefinition } from '@/spreadsheet-import/types/SpreadsheetImportFieldValidationDefinition';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { IconComponent } from 'twenty-ui/display';
|
||||
|
||||
export type SpreadsheetImportField = {
|
||||
export type SpreadsheetImportField<T extends string> = {
|
||||
// Icon
|
||||
Icon: IconComponent | null | undefined;
|
||||
// UI-facing field label
|
||||
label: string;
|
||||
// Field's unique identifier
|
||||
key: string;
|
||||
// Field's metadata item id - same for all associated nested fields
|
||||
fieldMetadataItemId: string;
|
||||
key: T;
|
||||
// UI-facing additional information displayed via tooltip and ? icon
|
||||
description?: string;
|
||||
// Alternate labels used for fields' auto-matching, e.g. "fname" -> "firstName"
|
||||
alternateMatches?: string[];
|
||||
// Validations used for field entries
|
||||
fieldValidationDefinitions?: SpreadsheetImportFieldValidationDefinition[];
|
||||
// Field entry component, default: Input
|
||||
fieldType: SpreadsheetImportFieldType;
|
||||
// Field metadata type
|
||||
fieldMetadataType: FieldMetadataType;
|
||||
// if true, it can be a composite sub-field or a relation connect field (or both)
|
||||
isNestedField: boolean;
|
||||
// can be true only if isNestedField is true
|
||||
isCompositeSubField?: boolean;
|
||||
// defined only if isCompositeSubField is true
|
||||
compositeSubFieldKey?: string;
|
||||
// can be true only if isNestedField is true
|
||||
isRelationConnectField?: boolean;
|
||||
// defined only if isRelationConnectField is true
|
||||
uniqueFieldMetadataItem?: FieldMetadataItem;
|
||||
// UI-facing values shown to user as field examples pre-upload phase
|
||||
example?: string;
|
||||
};
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
import { IconComponent } from 'twenty-ui/display';
|
||||
|
||||
export type SpreadsheetImportFieldOption = {
|
||||
Icon: IconComponent | null | undefined;
|
||||
value: string;
|
||||
label: string;
|
||||
shortLabelForNestedField?: string;
|
||||
disabled?: boolean;
|
||||
fieldMetadataTypeLabel?: string;
|
||||
isNestedField?: boolean;
|
||||
fieldMetadataItemId?: string;
|
||||
};
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
import { SpreadsheetImportField } from '@/spreadsheet-import/types/SpreadsheetImportField';
|
||||
import { ReadonlyDeep } from 'type-fest';
|
||||
|
||||
export type SpreadsheetImportFields = ReadonlyDeep<SpreadsheetImportField[]>;
|
||||
export type SpreadsheetImportFields<T extends string> = ReadonlyDeep<
|
||||
SpreadsheetImportField<T>[]
|
||||
>;
|
||||
|
||||
+5
-4
@@ -1,8 +1,9 @@
|
||||
import { ImportedStructuredRowMetadata } from '@/spreadsheet-import/steps/components/ValidationStep/types';
|
||||
import { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow';
|
||||
|
||||
export type SpreadsheetImportImportValidationResult = {
|
||||
validStructuredRows: ImportedStructuredRow[];
|
||||
invalidStructuredRows: ImportedStructuredRow[];
|
||||
allStructuredRows: (ImportedStructuredRow & ImportedStructuredRowMetadata)[];
|
||||
export type SpreadsheetImportImportValidationResult<T extends string> = {
|
||||
validStructuredRows: ImportedStructuredRow<T>[];
|
||||
invalidStructuredRows: ImportedStructuredRow<T>[];
|
||||
allStructuredRows: (ImportedStructuredRow<T> &
|
||||
ImportedStructuredRowMetadata)[];
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
export type ImportedStructuredRow = {
|
||||
[key: string]: string | boolean | undefined;
|
||||
export type ImportedStructuredRow<T extends string> = {
|
||||
[key in T]: string | boolean | undefined;
|
||||
};
|
||||
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
import { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow';
|
||||
import { SpreadsheetImportInfo } from './SpreadsheetImportInfo';
|
||||
|
||||
export type SpreadsheetImportRowHook = (
|
||||
row: ImportedStructuredRow,
|
||||
addError: (fieldKey: string, error: SpreadsheetImportInfo) => void,
|
||||
table: ImportedStructuredRow[],
|
||||
) => ImportedStructuredRow;
|
||||
export type SpreadsheetImportRowHook<T extends string> = (
|
||||
row: ImportedStructuredRow<T>,
|
||||
addError: (fieldKey: T, error: SpreadsheetImportInfo) => void,
|
||||
table: ImportedStructuredRow<T>[],
|
||||
) => ImportedStructuredRow<T>;
|
||||
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
import { ImportedStructuredRow } from './SpreadsheetImportImportedStructuredRow';
|
||||
import { SpreadsheetImportInfo } from './SpreadsheetImportInfo';
|
||||
|
||||
export type SpreadsheetImportTableHook = (
|
||||
table: ImportedStructuredRow[],
|
||||
export type SpreadsheetImportTableHook<T extends string> = (
|
||||
table: ImportedStructuredRow<T>[],
|
||||
addError: (
|
||||
rowIndex: number,
|
||||
fieldKey: string,
|
||||
fieldKey: T,
|
||||
error: SpreadsheetImportInfo,
|
||||
) => void,
|
||||
) => ImportedStructuredRow[];
|
||||
) => ImportedStructuredRow<T>[];
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
export type SpreadsheetMatchedOptions = {
|
||||
export type SpreadsheetMatchedOptions<T> = {
|
||||
entry: string;
|
||||
value?: string;
|
||||
value?: T;
|
||||
};
|
||||
|
||||
+28
-29
@@ -9,16 +9,17 @@ import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
describe('addErrorsAndRunHooks', () => {
|
||||
const requiredField = {
|
||||
type FullData = ImportedStructuredRow<'name' | 'age' | 'country'>;
|
||||
const requiredField: SpreadsheetImportField<'name'> = {
|
||||
key: 'name',
|
||||
label: 'Name',
|
||||
fieldValidationDefinitions: [{ rule: 'required' }],
|
||||
Icon: null,
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
} as SpreadsheetImportField;
|
||||
};
|
||||
|
||||
const regexField = {
|
||||
const regexField: SpreadsheetImportField<'age'> = {
|
||||
key: 'age',
|
||||
label: 'Age',
|
||||
fieldValidationDefinitions: [
|
||||
@@ -27,20 +28,18 @@ describe('addErrorsAndRunHooks', () => {
|
||||
Icon: null,
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.NUMBER,
|
||||
} as SpreadsheetImportField;
|
||||
};
|
||||
|
||||
const uniqueField = {
|
||||
const uniqueField: SpreadsheetImportField<'country'> = {
|
||||
key: 'country',
|
||||
label: 'Country',
|
||||
fieldValidationDefinitions: [{ rule: 'unique' }],
|
||||
Icon: null,
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.SELECT,
|
||||
fieldMetadataItemId: '2',
|
||||
isNestedField: false,
|
||||
} as SpreadsheetImportField;
|
||||
};
|
||||
|
||||
const functionValidationFieldTrue = {
|
||||
const functionValidationFieldTrue: SpreadsheetImportField<'email'> = {
|
||||
key: 'email',
|
||||
label: 'Email',
|
||||
fieldValidationDefinitions: [
|
||||
@@ -53,11 +52,9 @@ describe('addErrorsAndRunHooks', () => {
|
||||
Icon: null,
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.EMAILS,
|
||||
fieldMetadataItemId: '1',
|
||||
isNestedField: false,
|
||||
} as SpreadsheetImportField;
|
||||
};
|
||||
|
||||
const functionValidationFieldFalse = {
|
||||
const functionValidationFieldFalse: SpreadsheetImportField<'email'> = {
|
||||
key: 'email',
|
||||
label: 'Email',
|
||||
fieldValidationDefinitions: [
|
||||
@@ -70,25 +67,23 @@ describe('addErrorsAndRunHooks', () => {
|
||||
Icon: null,
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.EMAILS,
|
||||
fieldMetadataItemId: '3',
|
||||
isNestedField: false,
|
||||
} as SpreadsheetImportField;
|
||||
};
|
||||
|
||||
const validData: ImportedStructuredRow = {
|
||||
const validData: ImportedStructuredRow<'name' | 'age'> = {
|
||||
name: 'John',
|
||||
age: '30',
|
||||
};
|
||||
const dataWithoutNameAndInvalidAge: ImportedStructuredRow = {
|
||||
const dataWithoutNameAndInvalidAge: ImportedStructuredRow<'name' | 'age'> = {
|
||||
name: '',
|
||||
age: 'Invalid',
|
||||
};
|
||||
const dataWithDuplicatedValue: ImportedStructuredRow = {
|
||||
const dataWithDuplicatedValue: FullData = {
|
||||
name: 'Alice',
|
||||
age: '40',
|
||||
country: 'Brazil',
|
||||
};
|
||||
|
||||
const data: ImportedStructuredRow[] = [
|
||||
const data: ImportedStructuredRow<'name' | 'age'>[] = [
|
||||
validData,
|
||||
dataWithoutNameAndInvalidAge,
|
||||
];
|
||||
@@ -118,14 +113,18 @@ describe('addErrorsAndRunHooks', () => {
|
||||
level: 'error',
|
||||
};
|
||||
|
||||
const rowHook: SpreadsheetImportRowHook = jest.fn((row, addError) => {
|
||||
addError('name', nameError);
|
||||
return row;
|
||||
});
|
||||
const tableHook: SpreadsheetImportTableHook = jest.fn((table, addError) => {
|
||||
addError(0, 'age', ageError);
|
||||
return table;
|
||||
});
|
||||
const rowHook: SpreadsheetImportRowHook<'name' | 'age'> = jest.fn(
|
||||
(row, addError) => {
|
||||
addError('name', nameError);
|
||||
return row;
|
||||
},
|
||||
);
|
||||
const tableHook: SpreadsheetImportTableHook<'name' | 'age'> = jest.fn(
|
||||
(table, addError) => {
|
||||
addError(0, 'age', ageError);
|
||||
return table;
|
||||
},
|
||||
);
|
||||
|
||||
it('should correctly call rowHook and tableHook and add errors', () => {
|
||||
const result = addErrorsAndRunHooks(
|
||||
@@ -180,7 +179,7 @@ describe('addErrorsAndRunHooks', () => {
|
||||
[
|
||||
dataWithDuplicatedValue,
|
||||
dataWithDuplicatedValue,
|
||||
] as unknown as ImportedStructuredRow[],
|
||||
] as unknown as FullData[],
|
||||
[uniqueField],
|
||||
);
|
||||
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import { SpreadsheetImportField } from '@/spreadsheet-import/types';
|
||||
import { findMatch } from '@/spreadsheet-import/utils/findMatch';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
describe('findMatch', () => {
|
||||
const defaultField: SpreadsheetImportField<'defaultField'> = {
|
||||
key: 'defaultField',
|
||||
Icon: null,
|
||||
label: 'label',
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
alternateMatches: ['Full Name', 'First Name'],
|
||||
};
|
||||
|
||||
const secondaryField: SpreadsheetImportField<'secondaryField'> = {
|
||||
key: 'secondaryField',
|
||||
Icon: null,
|
||||
label: 'label',
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
};
|
||||
|
||||
const fields = [defaultField, secondaryField];
|
||||
|
||||
it('should return the matching field if the header matches exactly with the key', () => {
|
||||
const autoMapDistance = 0;
|
||||
|
||||
const result = findMatch(defaultField.key, fields, autoMapDistance);
|
||||
|
||||
expect(result).toBe(defaultField.key);
|
||||
});
|
||||
|
||||
it('should return the matching field if the header matches exactly one of the alternate matches', () => {
|
||||
const autoMapDistance = 0;
|
||||
|
||||
const result = findMatch(
|
||||
defaultField.alternateMatches?.[0] ?? '',
|
||||
fields,
|
||||
autoMapDistance,
|
||||
);
|
||||
|
||||
expect(result).toBe(defaultField.key);
|
||||
});
|
||||
|
||||
it('should return the matching field if the header matches partially one of the alternate matches', () => {
|
||||
const header = 'First';
|
||||
const autoMapDistance = 5;
|
||||
|
||||
const result = findMatch(header, fields, autoMapDistance);
|
||||
|
||||
expect(result).toBe(defaultField.key);
|
||||
});
|
||||
|
||||
it('should return the matching field if the header matches partially both of the alternate matches', () => {
|
||||
const header = 'Name';
|
||||
const autoMapDistance = 5;
|
||||
|
||||
const result = findMatch(header, fields, autoMapDistance);
|
||||
|
||||
expect(result).toBe(defaultField.key);
|
||||
});
|
||||
|
||||
it('should return undefined if no exact match or alternate match is found within the auto map distance', () => {
|
||||
const header = 'Header';
|
||||
const autoMapDistance = 2;
|
||||
|
||||
const result = findMatch(header, fields, autoMapDistance);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return the matching field with the smallest Levenshtein distance if within auto map distance', () => {
|
||||
const header = 'Name'.split('').reverse().join('');
|
||||
const autoMapDistance = 100;
|
||||
|
||||
const result = findMatch(header, fields, autoMapDistance);
|
||||
|
||||
expect(result).toBe(defaultField.key);
|
||||
});
|
||||
|
||||
it('should return undefined if no match is found within auto map distance', () => {
|
||||
const header = 'Name'.split('').reverse().join('');
|
||||
const autoMapDistance = 1;
|
||||
|
||||
const result = findMatch(header, fields, autoMapDistance);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
+9
-11
@@ -7,7 +7,7 @@ import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetCol
|
||||
import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
const nameField: SpreadsheetImportField = {
|
||||
const nameField: SpreadsheetImportField<'Name'> = {
|
||||
key: 'Name',
|
||||
label: 'Name',
|
||||
Icon: null,
|
||||
@@ -15,11 +15,9 @@ const nameField: SpreadsheetImportField = {
|
||||
type: 'input',
|
||||
},
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
fieldMetadataItemId: '1',
|
||||
isNestedField: false,
|
||||
};
|
||||
|
||||
const ageField: SpreadsheetImportField = {
|
||||
const ageField: SpreadsheetImportField<'Age'> = {
|
||||
key: 'Age',
|
||||
label: 'Age',
|
||||
Icon: null,
|
||||
@@ -27,37 +25,37 @@ const ageField: SpreadsheetImportField = {
|
||||
type: 'input',
|
||||
},
|
||||
fieldMetadataType: FieldMetadataType.NUMBER,
|
||||
fieldMetadataItemId: '2',
|
||||
isNestedField: false,
|
||||
};
|
||||
|
||||
const validations: SpreadsheetImportFieldValidationDefinition[] = [
|
||||
{ rule: 'required' },
|
||||
];
|
||||
const nameFieldWithValidations: SpreadsheetImportField = {
|
||||
const nameFieldWithValidations: SpreadsheetImportField<'Name'> = {
|
||||
...nameField,
|
||||
fieldValidationDefinitions: validations,
|
||||
};
|
||||
const ageFieldWithValidations: SpreadsheetImportField = {
|
||||
const ageFieldWithValidations: SpreadsheetImportField<'Age'> = {
|
||||
...ageField,
|
||||
fieldValidationDefinitions: validations,
|
||||
};
|
||||
|
||||
const nameColumn: SpreadsheetColumn = {
|
||||
type ColumnValues = 'Name' | 'Age';
|
||||
|
||||
const nameColumn: SpreadsheetColumn<ColumnValues> = {
|
||||
type: SpreadsheetColumnType.matched,
|
||||
index: 0,
|
||||
header: '',
|
||||
value: 'Name',
|
||||
};
|
||||
|
||||
const ageColumn: SpreadsheetColumn = {
|
||||
const ageColumn: SpreadsheetColumn<ColumnValues> = {
|
||||
type: SpreadsheetColumnType.matched,
|
||||
index: 0,
|
||||
header: '',
|
||||
value: 'Age',
|
||||
};
|
||||
|
||||
const extraColumn: SpreadsheetColumn = {
|
||||
const extraColumn: SpreadsheetColumn<ColumnValues> = {
|
||||
type: SpreadsheetColumnType.matched,
|
||||
index: 0,
|
||||
header: '',
|
||||
|
||||
+1
-5
@@ -17,7 +17,7 @@ describe('getFieldOptions', () => {
|
||||
value: 'Three',
|
||||
},
|
||||
];
|
||||
const fields: SpreadsheetImportField[] = [
|
||||
const fields: SpreadsheetImportField<'Options' | 'Name'>[] = [
|
||||
{
|
||||
key: 'Options',
|
||||
Icon: null,
|
||||
@@ -27,8 +27,6 @@ describe('getFieldOptions', () => {
|
||||
options: optionsArray,
|
||||
},
|
||||
fieldMetadataType: FieldMetadataType.SELECT,
|
||||
fieldMetadataItemId: '1',
|
||||
isNestedField: false,
|
||||
},
|
||||
{
|
||||
key: 'Name',
|
||||
@@ -38,8 +36,6 @@ describe('getFieldOptions', () => {
|
||||
type: 'input',
|
||||
},
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
fieldMetadataItemId: '2',
|
||||
isNestedField: false,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
import { SpreadsheetImportField } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
||||
import { getMatchedColumns } from '@/spreadsheet-import/utils/getMatchedColumns';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
describe('getMatchedColumns', () => {
|
||||
const columns: SpreadsheetColumn<string>[] = [
|
||||
{
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'Name',
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
header: 'Location',
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'Location',
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
header: 'Age',
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'Age',
|
||||
},
|
||||
];
|
||||
|
||||
const fields: SpreadsheetImportField<string>[] = [
|
||||
{
|
||||
key: 'Name',
|
||||
label: 'Name',
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
Icon: null,
|
||||
},
|
||||
{
|
||||
key: 'Location',
|
||||
label: 'Location',
|
||||
fieldType: { type: 'select', options: [] },
|
||||
fieldMetadataType: FieldMetadataType.POSITION,
|
||||
Icon: null,
|
||||
},
|
||||
{
|
||||
key: 'Age',
|
||||
label: 'Age',
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.NUMBER,
|
||||
Icon: null,
|
||||
},
|
||||
];
|
||||
|
||||
const data = [
|
||||
['John', 'New York'],
|
||||
['Alice', 'Los Angeles'],
|
||||
];
|
||||
|
||||
const autoMapDistance = 2;
|
||||
|
||||
it('should return matched columns for each field', () => {
|
||||
const result = getMatchedColumns(columns, fields, data, autoMapDistance);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'Name',
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
header: 'Location',
|
||||
type: SpreadsheetColumnType.matchedSelect,
|
||||
value: 'Location',
|
||||
matchedOptions: [
|
||||
{
|
||||
entry: 'New York',
|
||||
},
|
||||
{
|
||||
entry: 'Los Angeles',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
header: 'Age',
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'Age',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle columns with duplicate values by choosing the closest match', () => {
|
||||
const columnsWithDuplicates: SpreadsheetColumn<string>[] = [
|
||||
{
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'Name',
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
header: 'Name',
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'Name',
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
header: 'Location',
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'Location',
|
||||
},
|
||||
];
|
||||
|
||||
const result = getMatchedColumns(
|
||||
columnsWithDuplicates,
|
||||
fields,
|
||||
data,
|
||||
autoMapDistance,
|
||||
);
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: SpreadsheetColumnType.empty,
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
index: 1,
|
||||
header: 'Name',
|
||||
type: SpreadsheetColumnType.matched,
|
||||
value: 'Name',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return initial columns when no auto match is found', () => {
|
||||
const unmatchedColumnsData: string[][] = [
|
||||
['John', 'New York', '30'],
|
||||
['Alice', 'Los Angeles', '25'],
|
||||
];
|
||||
|
||||
const unmatchedFields: SpreadsheetImportField<string>[] = [
|
||||
{
|
||||
key: 'Hobby',
|
||||
label: 'Hobby',
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
Icon: null,
|
||||
},
|
||||
{
|
||||
key: 'Interest',
|
||||
label: 'Interest',
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
Icon: null,
|
||||
},
|
||||
];
|
||||
|
||||
const result = getMatchedColumns(
|
||||
columns,
|
||||
unmatchedFields,
|
||||
unmatchedColumnsData,
|
||||
autoMapDistance,
|
||||
);
|
||||
|
||||
expect(result).toEqual(columns);
|
||||
});
|
||||
});
|
||||
+11
-13
@@ -6,7 +6,7 @@ import { normalizeTableData } from '@/spreadsheet-import/utils/normalizeTableDat
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
describe('normalizeTableData', () => {
|
||||
const columns: SpreadsheetColumn[] = [
|
||||
const columns: SpreadsheetColumn<string>[] = [
|
||||
{
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
@@ -27,7 +27,7 @@ describe('normalizeTableData', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const fields = [
|
||||
const fields: SpreadsheetImportField<string>[] = [
|
||||
{
|
||||
key: 'name',
|
||||
label: 'Name',
|
||||
@@ -51,7 +51,7 @@ describe('normalizeTableData', () => {
|
||||
fieldMetadataType: FieldMetadataType.BOOLEAN,
|
||||
Icon: null,
|
||||
},
|
||||
] as SpreadsheetImportField[];
|
||||
];
|
||||
|
||||
const rawData = [
|
||||
['John', '30', 'Yes'],
|
||||
@@ -70,7 +70,7 @@ describe('normalizeTableData', () => {
|
||||
});
|
||||
|
||||
it('should normalize matchedCheckbox values and handle booleanMatches', () => {
|
||||
const columns: SpreadsheetColumn[] = [
|
||||
const columns: SpreadsheetColumn<string>[] = [
|
||||
{
|
||||
index: 0,
|
||||
header: 'Active',
|
||||
@@ -79,7 +79,7 @@ describe('normalizeTableData', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const fields = [
|
||||
const fields: SpreadsheetImportField<string>[] = [
|
||||
{
|
||||
key: 'active',
|
||||
label: 'Active',
|
||||
@@ -89,10 +89,8 @@ describe('normalizeTableData', () => {
|
||||
},
|
||||
fieldMetadataType: FieldMetadataType.BOOLEAN,
|
||||
Icon: null,
|
||||
fieldMetadataItemId: '1',
|
||||
isNestedField: false,
|
||||
},
|
||||
] as SpreadsheetImportField[];
|
||||
];
|
||||
|
||||
const rawData = [['Yes'], ['No'], ['OtherValue']];
|
||||
|
||||
@@ -102,7 +100,7 @@ describe('normalizeTableData', () => {
|
||||
});
|
||||
|
||||
it('should map matchedSelect and matchedSelectOptions values correctly', () => {
|
||||
const columns: SpreadsheetColumn[] = [
|
||||
const columns: SpreadsheetColumn<string>[] = [
|
||||
{
|
||||
index: 0,
|
||||
header: 'Number',
|
||||
@@ -115,7 +113,7 @@ describe('normalizeTableData', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const fields = [
|
||||
const fields: SpreadsheetImportField<string>[] = [
|
||||
{
|
||||
key: 'number',
|
||||
label: 'Number',
|
||||
@@ -129,7 +127,7 @@ describe('normalizeTableData', () => {
|
||||
fieldMetadataType: FieldMetadataType.SELECT,
|
||||
Icon: null,
|
||||
},
|
||||
] as SpreadsheetImportField[];
|
||||
];
|
||||
|
||||
const rawData = [['One'], ['Two'], ['OtherValue']];
|
||||
|
||||
@@ -143,7 +141,7 @@ describe('normalizeTableData', () => {
|
||||
});
|
||||
|
||||
it('should handle empty and ignored columns', () => {
|
||||
const columns: SpreadsheetColumn[] = [
|
||||
const columns: SpreadsheetColumn<string>[] = [
|
||||
{ index: 0, header: 'Empty', type: SpreadsheetColumnType.empty },
|
||||
{ index: 1, header: 'Ignored', type: SpreadsheetColumnType.ignored },
|
||||
];
|
||||
@@ -156,7 +154,7 @@ describe('normalizeTableData', () => {
|
||||
});
|
||||
|
||||
it('should handle unrecognized column types and return empty object', () => {
|
||||
const columns: SpreadsheetColumns = [
|
||||
const columns: SpreadsheetColumns<string> = [
|
||||
{
|
||||
index: 0,
|
||||
header: 'Unrecognized',
|
||||
|
||||
+7
-7
@@ -5,15 +5,15 @@ import { setColumn } from '@/spreadsheet-import/utils/setColumn';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
describe('setColumn', () => {
|
||||
const defaultField = {
|
||||
const defaultField: SpreadsheetImportField<'Name'> = {
|
||||
Icon: null,
|
||||
label: 'label',
|
||||
key: 'Name',
|
||||
fieldType: { type: 'input' },
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
} as SpreadsheetImportField;
|
||||
};
|
||||
|
||||
const oldColumn: SpreadsheetColumn = {
|
||||
const oldColumn: SpreadsheetColumn<'oldValue'> = {
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: SpreadsheetColumnType.matched,
|
||||
@@ -27,7 +27,7 @@ describe('setColumn', () => {
|
||||
type: 'select',
|
||||
options: [{ value: 'John' }, { value: 'Alice' }],
|
||||
},
|
||||
} as SpreadsheetImportField;
|
||||
} as SpreadsheetImportField<'Name'>;
|
||||
|
||||
const data = [['John'], ['Alice']];
|
||||
const result = setColumn(oldColumn, field, data);
|
||||
@@ -54,7 +54,7 @@ describe('setColumn', () => {
|
||||
const field = {
|
||||
...defaultField,
|
||||
fieldType: { type: 'checkbox' },
|
||||
} as SpreadsheetImportField;
|
||||
} as SpreadsheetImportField<'Name'>;
|
||||
|
||||
const result = setColumn(oldColumn, field);
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('setColumn', () => {
|
||||
const field = {
|
||||
...defaultField,
|
||||
fieldType: { type: 'input' },
|
||||
} as SpreadsheetImportField;
|
||||
} as SpreadsheetImportField<'Name'>;
|
||||
|
||||
const result = setColumn(oldColumn, field);
|
||||
|
||||
@@ -86,7 +86,7 @@ describe('setColumn', () => {
|
||||
const field = {
|
||||
...defaultField,
|
||||
fieldType: { type: 'unknown' },
|
||||
} as unknown as SpreadsheetImportField;
|
||||
} as unknown as SpreadsheetImportField<'Name'>;
|
||||
|
||||
const result = setColumn(oldColumn, field);
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { setIgnoreColumn } from '@/spreadsheet-import/utils/setIgnoreColumn';
|
||||
|
||||
describe('setIgnoreColumn', () => {
|
||||
it('should return a column with type "ignored"', () => {
|
||||
const column: SpreadsheetColumn = {
|
||||
const column: SpreadsheetColumn<'John'> = {
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: SpreadsheetColumnType.matched,
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ import { setSubColumn } from '@/spreadsheet-import/utils/setSubColumn';
|
||||
|
||||
describe('setSubColumn', () => {
|
||||
it('should return a matchedSelectColumn with updated matchedOptions', () => {
|
||||
const oldColumn: SpreadsheetColumn = {
|
||||
const oldColumn: SpreadsheetColumn<'John' | ''> = {
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: SpreadsheetColumnType.matchedSelect,
|
||||
@@ -32,7 +32,7 @@ describe('setSubColumn', () => {
|
||||
});
|
||||
|
||||
it('should return a matchedSelectOptionsColumn with updated matchedOptions', () => {
|
||||
const oldColumn: SpreadsheetColumn = {
|
||||
const oldColumn: SpreadsheetColumn<'John' | 'Jane'> = {
|
||||
index: 0,
|
||||
header: 'Name',
|
||||
type: SpreadsheetColumnType.matchedSelectOptions,
|
||||
|
||||
@@ -15,17 +15,17 @@ import {
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export const addErrorsAndRunHooks = (
|
||||
data: (ImportedStructuredRow & Partial<ImportedStructuredRowMetadata>)[],
|
||||
fields: SpreadsheetImportFields,
|
||||
rowHook?: SpreadsheetImportRowHook,
|
||||
tableHook?: SpreadsheetImportTableHook,
|
||||
): (ImportedStructuredRow & ImportedStructuredRowMetadata)[] => {
|
||||
export const addErrorsAndRunHooks = <T extends string>(
|
||||
data: (ImportedStructuredRow<T> & Partial<ImportedStructuredRowMetadata>)[],
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
rowHook?: SpreadsheetImportRowHook<T>,
|
||||
tableHook?: SpreadsheetImportTableHook<T>,
|
||||
): (ImportedStructuredRow<T> & ImportedStructuredRowMetadata)[] => {
|
||||
const errors: Errors = {};
|
||||
|
||||
const addHookError = (
|
||||
rowIndex: number,
|
||||
fieldKey: string,
|
||||
fieldKey: T,
|
||||
error: SpreadsheetImportInfo,
|
||||
) => {
|
||||
errors[rowIndex] = {
|
||||
@@ -48,7 +48,7 @@ export const addErrorsAndRunHooks = (
|
||||
field.fieldValidationDefinitions?.forEach((fieldValidationDefinition) => {
|
||||
switch (fieldValidationDefinition.rule) {
|
||||
case 'unique': {
|
||||
const values = data.map((entry) => entry[field.key]);
|
||||
const values = data.map((entry) => entry[field.key as T]);
|
||||
|
||||
const taken = new Set(); // Set of items used at least once
|
||||
const duplicates = new Set(); // Set of items used multiple times
|
||||
@@ -87,9 +87,9 @@ export const addErrorsAndRunHooks = (
|
||||
case 'required': {
|
||||
data.forEach((entry, index) => {
|
||||
if (
|
||||
entry[field.key] === null ||
|
||||
entry[field.key] === undefined ||
|
||||
entry[field.key] === ''
|
||||
entry[field.key as T] === null ||
|
||||
entry[field.key as T] === undefined ||
|
||||
entry[field.key as T] === ''
|
||||
) {
|
||||
errors[index] = {
|
||||
...errors[index],
|
||||
@@ -156,17 +156,14 @@ export const addErrorsAndRunHooks = (
|
||||
if (!('__index' in value)) {
|
||||
value.__index = v4();
|
||||
}
|
||||
const newValue = value as ImportedStructuredRow &
|
||||
const newValue = value as ImportedStructuredRow<T> &
|
||||
ImportedStructuredRowMetadata;
|
||||
|
||||
if (isDefined(errors[index])) {
|
||||
return { ...newValue, __errors: errors[index] } as ImportedStructuredRow &
|
||||
ImportedStructuredRowMetadata;
|
||||
return { ...newValue, __errors: errors[index] };
|
||||
}
|
||||
|
||||
if (isUndefinedOrNull(errors[index]) && isDefined(value?.__errors)) {
|
||||
return { ...newValue, __errors: null } as ImportedStructuredRow &
|
||||
ImportedStructuredRowMetadata;
|
||||
return { ...newValue, __errors: null };
|
||||
}
|
||||
return newValue;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||
import lavenstein from 'js-levenshtein';
|
||||
|
||||
type AutoMatchAccumulator<T> = {
|
||||
distance: number;
|
||||
value: T;
|
||||
};
|
||||
|
||||
export const findMatch = <T extends string>(
|
||||
header: string,
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
autoMapDistance: number,
|
||||
): T | undefined => {
|
||||
const smallestValue = fields.reduce<AutoMatchAccumulator<T>>((acc, field) => {
|
||||
const distance = Math.min(
|
||||
...[
|
||||
lavenstein(field.key, header),
|
||||
...(field.alternateMatches?.map((alternate) =>
|
||||
lavenstein(alternate, header),
|
||||
) || []),
|
||||
],
|
||||
);
|
||||
return distance < acc.distance || acc.distance === undefined
|
||||
? ({ value: field.key, distance } as AutoMatchAccumulator<T>)
|
||||
: acc;
|
||||
}, {} as AutoMatchAccumulator<T>);
|
||||
return smallestValue.distance <= autoMapDistance
|
||||
? smallestValue.value
|
||||
: undefined;
|
||||
};
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||
|
||||
export const findUnmatchedRequiredFields = (
|
||||
fields: SpreadsheetImportFields,
|
||||
columns: SpreadsheetColumns,
|
||||
export const findUnmatchedRequiredFields = <T extends string>(
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
columns: SpreadsheetColumns<T>,
|
||||
) =>
|
||||
fields
|
||||
.filter((field) =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||
|
||||
export const getFieldOptions = (
|
||||
fields: SpreadsheetImportFields,
|
||||
export const getFieldOptions = <T extends string>(
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
fieldKey: string,
|
||||
) => {
|
||||
const field = fields.find(({ key }) => fieldKey === key);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import lavenstein from 'js-levenshtein';
|
||||
|
||||
import { MatchColumnsStepProps } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
|
||||
import {
|
||||
SpreadsheetImportField,
|
||||
SpreadsheetImportFields,
|
||||
} from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { findMatch } from './findMatch';
|
||||
import { setColumn } from './setColumn';
|
||||
|
||||
export const getMatchedColumns = <T extends string>(
|
||||
columns: SpreadsheetColumns<T>,
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
data: MatchColumnsStepProps['data'],
|
||||
autoMapDistance: number,
|
||||
) =>
|
||||
columns.reduce<SpreadsheetColumn<T>[]>((arr, column) => {
|
||||
const autoMatch = findMatch(column.header, fields, autoMapDistance);
|
||||
if (isDefined(autoMatch)) {
|
||||
const field = fields.find(
|
||||
(field) => field.key === autoMatch,
|
||||
) as SpreadsheetImportField<T>;
|
||||
const duplicateIndex = arr.findIndex(
|
||||
(column) => 'value' in column && column.value === field.key,
|
||||
);
|
||||
const duplicate = arr[duplicateIndex];
|
||||
if (duplicate && 'value' in duplicate) {
|
||||
return lavenstein(duplicate.value, duplicate.header) <
|
||||
lavenstein(autoMatch, column.header)
|
||||
? [
|
||||
...arr.slice(0, duplicateIndex),
|
||||
setColumn(arr[duplicateIndex], field, data),
|
||||
...arr.slice(duplicateIndex + 1),
|
||||
setColumn(column),
|
||||
]
|
||||
: [
|
||||
...arr.slice(0, duplicateIndex),
|
||||
setColumn(arr[duplicateIndex]),
|
||||
...arr.slice(duplicateIndex + 1),
|
||||
setColumn(column, field, data),
|
||||
];
|
||||
} else {
|
||||
return [...arr, setColumn(column, field, data)];
|
||||
}
|
||||
} else {
|
||||
return [...arr, column];
|
||||
}
|
||||
}, []);
|
||||
+7
-7
@@ -11,16 +11,16 @@ import { setColumn } from '@/spreadsheet-import/utils/setColumn';
|
||||
import Fuse from 'fuse.js';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const getMatchedColumnsWithFuse = ({
|
||||
export const getMatchedColumnsWithFuse = <T extends string>({
|
||||
columns,
|
||||
fields,
|
||||
data,
|
||||
}: {
|
||||
columns: SpreadsheetColumns;
|
||||
fields: SpreadsheetImportFields;
|
||||
columns: SpreadsheetColumns<T>;
|
||||
fields: SpreadsheetImportFields<T>;
|
||||
data: MatchColumnsStepProps['data'];
|
||||
}) => {
|
||||
const matchedColumns: SpreadsheetColumn[] = [];
|
||||
const matchedColumns: SpreadsheetColumn<T>[] = [];
|
||||
|
||||
const fieldsToSearch = new Fuse(fields, {
|
||||
keys: ['label'],
|
||||
@@ -30,8 +30,8 @@ export const getMatchedColumnsWithFuse = ({
|
||||
});
|
||||
|
||||
const suggestedFieldsByColumnHeader: Record<
|
||||
SpreadsheetColumn['header'],
|
||||
SpreadsheetImportField[]
|
||||
SpreadsheetColumn<T>['header'],
|
||||
SpreadsheetImportField<T>[]
|
||||
> = {};
|
||||
|
||||
for (const column of columns) {
|
||||
@@ -58,7 +58,7 @@ export const getMatchedColumnsWithFuse = ({
|
||||
);
|
||||
|
||||
suggestedFieldsByColumnHeader[column.header] = fieldsThatMatch.map(
|
||||
(match) => match.item as SpreadsheetImportField,
|
||||
(match) => match.item as SpreadsheetImportField<T>,
|
||||
);
|
||||
|
||||
if (isFirstMatchValid && isFieldStillUnmatched) {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export const getShortNestedFieldLabel = (label: string) => {
|
||||
return label.split(' / ').slice(1).join(' / ');
|
||||
};
|
||||
@@ -9,10 +9,10 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { z } from 'zod';
|
||||
import { normalizeCheckboxValue } from './normalizeCheckboxValue';
|
||||
|
||||
export const normalizeTableData = (
|
||||
columns: SpreadsheetColumns,
|
||||
export const normalizeTableData = <T extends string>(
|
||||
columns: SpreadsheetColumns<T>,
|
||||
data: ImportedRow[],
|
||||
fields: SpreadsheetImportFields,
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
) =>
|
||||
data.map((row) =>
|
||||
columns.reduce((acc, column, index) => {
|
||||
@@ -101,5 +101,5 @@ export const normalizeTableData = (
|
||||
default:
|
||||
return acc;
|
||||
}
|
||||
}, {} as ImportedStructuredRow),
|
||||
}, {} as ImportedStructuredRow<T>),
|
||||
);
|
||||
|
||||
@@ -9,17 +9,17 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { z } from 'zod';
|
||||
import { uniqueEntries } from './uniqueEntries';
|
||||
|
||||
export const setColumn = (
|
||||
oldColumn: SpreadsheetColumn,
|
||||
field?: SpreadsheetImportField,
|
||||
export const setColumn = <T extends string>(
|
||||
oldColumn: SpreadsheetColumn<T>,
|
||||
field?: SpreadsheetImportField<T>,
|
||||
data?: MatchColumnsStepProps['data'],
|
||||
): SpreadsheetColumn => {
|
||||
): SpreadsheetColumn<T> => {
|
||||
if (field?.fieldType.type === 'select') {
|
||||
const fieldOptions = field.fieldType.options;
|
||||
const uniqueData = uniqueEntries(
|
||||
data || [],
|
||||
oldColumn.index,
|
||||
) as SpreadsheetMatchedOptions[];
|
||||
) as SpreadsheetMatchedOptions<T>[];
|
||||
|
||||
const matchedOptions = uniqueData.map((record) => {
|
||||
const value = fieldOptions.find(
|
||||
@@ -28,8 +28,8 @@ export const setColumn = (
|
||||
fieldOption.label === record.entry,
|
||||
)?.value;
|
||||
return value
|
||||
? ({ ...record, value } as SpreadsheetMatchedOptions)
|
||||
: (record as SpreadsheetMatchedOptions);
|
||||
? ({ ...record, value } as SpreadsheetMatchedOptions<T>)
|
||||
: (record as SpreadsheetMatchedOptions<T>);
|
||||
});
|
||||
const allMatched =
|
||||
matchedOptions.filter((o) => o.value).length === uniqueData?.length;
|
||||
@@ -77,8 +77,8 @@ export const setColumn = (
|
||||
fieldOption.value === entry || fieldOption.label === entry,
|
||||
)?.value;
|
||||
return value
|
||||
? ({ entry, value } as SpreadsheetMatchedOptions)
|
||||
: ({ entry } as SpreadsheetMatchedOptions);
|
||||
? ({ entry, value } as SpreadsheetMatchedOptions<T>)
|
||||
: ({ entry } as SpreadsheetMatchedOptions<T>);
|
||||
});
|
||||
const areAllMatched =
|
||||
matchedOptions.filter((option) => option.value).length ===
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
||||
|
||||
export const setIgnoreColumn = ({
|
||||
export const setIgnoreColumn = <T extends string>({
|
||||
header,
|
||||
index,
|
||||
}: SpreadsheetColumn): SpreadsheetColumn => ({
|
||||
}: SpreadsheetColumn<T>): SpreadsheetColumn<T> => ({
|
||||
header,
|
||||
index,
|
||||
type: SpreadsheetColumnType.ignored,
|
||||
|
||||
@@ -5,13 +5,15 @@ import {
|
||||
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
||||
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
|
||||
|
||||
export const setSubColumn = (
|
||||
export const setSubColumn = <T>(
|
||||
oldColumn:
|
||||
| SpreadsheetMatchedSelectColumn
|
||||
| SpreadsheetMatchedSelectOptionsColumn,
|
||||
| SpreadsheetMatchedSelectColumn<T>
|
||||
| SpreadsheetMatchedSelectOptionsColumn<T>,
|
||||
entry: string,
|
||||
value: string,
|
||||
): SpreadsheetMatchedSelectColumn | SpreadsheetMatchedSelectOptionsColumn => {
|
||||
):
|
||||
| SpreadsheetMatchedSelectColumn<T>
|
||||
| SpreadsheetMatchedSelectOptionsColumn<T> => {
|
||||
const shouldUnselectValue =
|
||||
oldColumn.matchedOptions.find((option) => option.entry === entry)?.value ===
|
||||
value;
|
||||
@@ -26,13 +28,13 @@ export const setSubColumn = (
|
||||
if (allMatched) {
|
||||
return {
|
||||
...oldColumn,
|
||||
matchedOptions: options as SpreadsheetMatchedOptions[],
|
||||
matchedOptions: options as SpreadsheetMatchedOptions<T>[],
|
||||
type: SpreadsheetColumnType.matchedSelectOptions,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...oldColumn,
|
||||
matchedOptions: options as SpreadsheetMatchedOptions[],
|
||||
matchedOptions: options as SpreadsheetMatchedOptions<T>[],
|
||||
type: SpreadsheetColumnType.matchedSelect,
|
||||
};
|
||||
}
|
||||
|
||||
+29
@@ -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;
|
||||
});
|
||||
};
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
import { getFieldMetadataTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFieldMetadataTypeLabel';
|
||||
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||
import { SpreadsheetImportFieldOption } from '@/spreadsheet-import/types/SpreadsheetImportFieldOption';
|
||||
import { getShortNestedFieldLabel } from '@/spreadsheet-import/utils/getShortNestedFieldLabel';
|
||||
import { ReadonlyDeep } from 'type-fest';
|
||||
|
||||
export const spreadsheetImportBuildFieldOptions = (
|
||||
fields: SpreadsheetImportFields,
|
||||
columns: SpreadsheetColumns,
|
||||
): readonly ReadonlyDeep<SpreadsheetImportFieldOption>[] => {
|
||||
return fields.map(
|
||||
({
|
||||
Icon,
|
||||
label,
|
||||
key,
|
||||
fieldMetadataType,
|
||||
isNestedField,
|
||||
fieldMetadataItemId,
|
||||
}) => {
|
||||
const isSelected = columns.some((column) => {
|
||||
if ('value' in column) {
|
||||
return column.value === key;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return {
|
||||
Icon: Icon,
|
||||
value: key,
|
||||
label,
|
||||
shortLabelForNestedField: isNestedField
|
||||
? getShortNestedFieldLabel(label)
|
||||
: undefined,
|
||||
disabled: isSelected,
|
||||
fieldMetadataTypeLabel: getFieldMetadataTypeLabel(fieldMetadataType),
|
||||
isNestedField: isNestedField,
|
||||
fieldMetadataItemId: fieldMetadataItemId,
|
||||
};
|
||||
},
|
||||
);
|
||||
};
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SpreadsheetImportFieldOption } from '@/spreadsheet-import/types/SpreadsheetImportFieldOption';
|
||||
|
||||
export const getSubFieldOptions = (
|
||||
fieldMetadataItem: FieldMetadataItem,
|
||||
options: readonly Readonly<SpreadsheetImportFieldOption>[],
|
||||
searchFilter: string,
|
||||
): readonly Readonly<SpreadsheetImportFieldOption>[] => {
|
||||
return options.filter(
|
||||
(option) =>
|
||||
option.fieldMetadataItemId === fieldMetadataItem.id &&
|
||||
option.label.toLowerCase().includes(searchFilter.toLowerCase()),
|
||||
);
|
||||
};
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { RelationType } from '~/generated/graphql';
|
||||
|
||||
export const hasNestedFields = (fieldMetadata: FieldMetadataItem) => {
|
||||
return (
|
||||
(fieldMetadata.type === FieldMetadataType.RELATION &&
|
||||
fieldMetadata.relation?.type === RelationType.MANY_TO_ONE) ||
|
||||
isCompositeFieldType(fieldMetadata.type)
|
||||
);
|
||||
};
|
||||
@@ -3,10 +3,10 @@ import uniqBy from 'lodash.uniqby';
|
||||
import { MatchColumnsStepProps } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
|
||||
|
||||
export const uniqueEntries = (
|
||||
export const uniqueEntries = <T extends string>(
|
||||
data: MatchColumnsStepProps['data'],
|
||||
index: number,
|
||||
): Partial<SpreadsheetMatchedOptions>[] =>
|
||||
): Partial<SpreadsheetMatchedOptions<T>>[] =>
|
||||
uniqBy(
|
||||
data.map((row) => ({ entry: row[index] })),
|
||||
'entry',
|
||||
|
||||
Reference in New Issue
Block a user