Improve CSV import sub-field selection (#11601)
This PR adds a better UX for selecting sub-fields when importing CSV files. Before : <img width="395" alt="image" src="https://github.com/user-attachments/assets/5a599e7d-ed07-4531-8306-9d70e7cfa37d" /> After : <img width="298" alt="image" src="https://github.com/user-attachments/assets/2be8a1df-d089-4341-970e-6db2b269141e" /> <img width="296" alt="image" src="https://github.com/user-attachments/assets/584285f4-4e71-4abd-9adf-11819cab0dc5" /> - A util `getSubFieldOptionKey` has been made to be able to reference the sub field in the `options` object of the spreadsheet import. - New components have been created : `MatchColumnSelectFieldSelectDropdownContent`, `MatchColumnSelectSubFieldSelectDropdownContent` and `MatchColumnSelectV2` - Extracted the hard-coded option do not import into a constant `DO_NOT_IMPORT_OPTION_KEY` - Passed `availableFieldMetadataItems` to spreadsheet global options so it's available anywhere in the hierarchy of components. --------- Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
FieldActorValue,
|
||||||
FieldAddressValue,
|
FieldAddressValue,
|
||||||
FieldCurrencyValue,
|
FieldCurrencyValue,
|
||||||
FieldEmailsValue,
|
FieldEmailsValue,
|
||||||
@ -46,5 +47,5 @@ export const COMPOSITE_FIELD_IMPORT_LABELS = {
|
|||||||
} satisfies Partial<CompositeFieldLabels<FieldRichTextV2Value>>,
|
} satisfies Partial<CompositeFieldLabels<FieldRichTextV2Value>>,
|
||||||
[FieldMetadataType.ACTOR]: {
|
[FieldMetadataType.ACTOR]: {
|
||||||
sourceLabel: 'Source',
|
sourceLabel: 'Source',
|
||||||
},
|
} satisfies Partial<CompositeFieldLabels<FieldActorValue>>,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
|||||||
import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels';
|
import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels';
|
||||||
import { AvailableFieldForImport } from '@/object-record/spreadsheet-import/types/AvailableFieldForImport';
|
import { AvailableFieldForImport } from '@/object-record/spreadsheet-import/types/AvailableFieldForImport';
|
||||||
import { getSpreadSheetFieldValidationDefinitions } from '@/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions';
|
import { getSpreadSheetFieldValidationDefinitions } from '@/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|
||||||
import { useIcons } from 'twenty-ui/display';
|
import { useIcons } from 'twenty-ui/display';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
type CompositeFieldType = keyof typeof COMPOSITE_FIELD_IMPORT_LABELS;
|
type CompositeFieldType = keyof typeof COMPOSITE_FIELD_IMPORT_LABELS;
|
||||||
|
|
||||||
@ -42,17 +42,17 @@ export const useBuildAvailableFieldsForImport = () => {
|
|||||||
validationTypeResolver?: ValidationTypeResolver,
|
validationTypeResolver?: ValidationTypeResolver,
|
||||||
) => {
|
) => {
|
||||||
Object.entries(COMPOSITE_FIELD_IMPORT_LABELS[fieldType]).forEach(
|
Object.entries(COMPOSITE_FIELD_IMPORT_LABELS[fieldType]).forEach(
|
||||||
([key, fieldLabel]) => {
|
([key, subFieldLabel]) => {
|
||||||
const label = `${fieldLabel} (${fieldMetadataItem.label})`;
|
const label = `${fieldMetadataItem.label} / ${subFieldLabel}`;
|
||||||
// Use the custom validation type if provided, otherwise use the field's type
|
// Use the custom validation type if provided, otherwise use the field's type
|
||||||
const validationType = validationTypeResolver
|
const validationType = validationTypeResolver
|
||||||
? validationTypeResolver(key, fieldLabel)
|
? validationTypeResolver(key, subFieldLabel)
|
||||||
: fieldMetadataItem.type;
|
: fieldMetadataItem.type;
|
||||||
|
|
||||||
availableFieldsForImport.push(
|
availableFieldsForImport.push(
|
||||||
createBaseField(fieldMetadataItem, {
|
createBaseField(fieldMetadataItem, {
|
||||||
label,
|
label,
|
||||||
key: `${fieldLabel} (${fieldMetadataItem.name})`,
|
key: `${subFieldLabel} (${fieldMetadataItem.name})`,
|
||||||
fieldValidationDefinitions:
|
fieldValidationDefinitions:
|
||||||
getSpreadSheetFieldValidationDefinitions(validationType, label),
|
getSpreadSheetFieldValidationDefinitions(validationType, label),
|
||||||
}),
|
}),
|
||||||
@ -137,6 +137,12 @@ export const useBuildAvailableFieldsForImport = () => {
|
|||||||
currencyValidationResolver,
|
currencyValidationResolver,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
[FieldMetadataType.ACTOR]: (fieldMetadataItem) => {
|
||||||
|
handleCompositeFieldWithLabels(
|
||||||
|
fieldMetadataItem,
|
||||||
|
FieldMetadataType.ACTOR,
|
||||||
|
);
|
||||||
|
},
|
||||||
[FieldMetadataType.RELATION]: (fieldMetadataItem) => {
|
[FieldMetadataType.RELATION]: (fieldMetadataItem) => {
|
||||||
const label = `${fieldMetadataItem.label} (ID)`;
|
const label = `${fieldMetadataItem.label} (ID)`;
|
||||||
availableFieldsForImport.push(
|
availableFieldsForImport.push(
|
||||||
|
|||||||
@ -74,6 +74,7 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
fields: availableFields,
|
fields: availableFields,
|
||||||
|
availableFieldMetadataItems,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels';
|
||||||
|
|
||||||
|
export const getSubFieldOptionKey = (
|
||||||
|
fieldMetadataItem: FieldMetadataItem,
|
||||||
|
subFieldName: string,
|
||||||
|
) => {
|
||||||
|
const subFieldNameLabelKey = `${subFieldName}Label`;
|
||||||
|
|
||||||
|
const subFieldLabel = (
|
||||||
|
(COMPOSITE_FIELD_IMPORT_LABELS as any)[fieldMetadataItem.type] as any
|
||||||
|
)[subFieldNameLabelKey];
|
||||||
|
|
||||||
|
const subFieldKey = `${subFieldLabel} (${fieldMetadataItem.name})`;
|
||||||
|
|
||||||
|
return subFieldKey;
|
||||||
|
};
|
||||||
@ -156,6 +156,7 @@ export const mockRsiValues = mockComponentBehaviourForTypes({
|
|||||||
await sleep(4000, (resolve) => resolve(data));
|
await sleep(4000, (resolve) => resolve(data));
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
availableFieldMetadataItems: []
|
||||||
});
|
});
|
||||||
|
|
||||||
export const editableTableInitialData = [
|
export const editableTableInitialData = [
|
||||||
|
|||||||
@ -1,138 +0,0 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
import { ReadonlyDeep } from 'type-fest';
|
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
|
||||||
|
|
||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
|
||||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
|
||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
|
||||||
import { useLingui } from '@lingui/react/macro';
|
|
||||||
import { AppTooltip } from 'twenty-ui/display';
|
|
||||||
import { SelectOption } from 'twenty-ui/input';
|
|
||||||
import { MenuItem, MenuItemSelect } from 'twenty-ui/navigation';
|
|
||||||
import { v4 } from 'uuid';
|
|
||||||
import { useUpdateEffect } from '~/hooks/useUpdateEffect';
|
|
||||||
|
|
||||||
interface MatchColumnSelectProps {
|
|
||||||
columnIndex: string;
|
|
||||||
onChange: (value: ReadonlyDeep<SelectOption> | null) => void;
|
|
||||||
value?: ReadonlyDeep<SelectOption>;
|
|
||||||
options: readonly ReadonlyDeep<SelectOption>[];
|
|
||||||
placeholder?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MatchColumnSelect = ({
|
|
||||||
onChange,
|
|
||||||
value,
|
|
||||||
options: initialOptions,
|
|
||||||
placeholder,
|
|
||||||
columnIndex,
|
|
||||||
}: MatchColumnSelectProps) => {
|
|
||||||
const dropdownId = `match-column-select-dropdown-${columnIndex}`;
|
|
||||||
|
|
||||||
const { closeDropdown } = useDropdown(dropdownId);
|
|
||||||
|
|
||||||
const [searchFilter, setSearchFilter] = useState('');
|
|
||||||
const [options, setOptions] = useState(initialOptions);
|
|
||||||
|
|
||||||
const handleSearchFilterChange = useCallback(
|
|
||||||
(text: string) => {
|
|
||||||
setOptions(
|
|
||||||
initialOptions.filter((option) =>
|
|
||||||
option.label.toLowerCase().includes(text.toLowerCase()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[initialOptions],
|
|
||||||
);
|
|
||||||
|
|
||||||
const debouncedHandleSearchFilter = useDebouncedCallback(
|
|
||||||
handleSearchFilterChange,
|
|
||||||
100,
|
|
||||||
{
|
|
||||||
leading: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = event.currentTarget.value;
|
|
||||||
|
|
||||||
setSearchFilter(value);
|
|
||||||
debouncedHandleSearchFilter(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (option: ReadonlyDeep<SelectOption>) => {
|
|
||||||
onChange(option);
|
|
||||||
closeDropdown();
|
|
||||||
};
|
|
||||||
|
|
||||||
useUpdateEffect(() => {
|
|
||||||
setOptions(initialOptions);
|
|
||||||
}, [initialOptions]);
|
|
||||||
|
|
||||||
const { t } = useLingui();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dropdown
|
|
||||||
dropdownId={dropdownId}
|
|
||||||
dropdownHotkeyScope={{
|
|
||||||
scope: dropdownId,
|
|
||||||
}}
|
|
||||||
dropdownPlacement="bottom-start"
|
|
||||||
clickableComponent={
|
|
||||||
<MenuItem
|
|
||||||
LeftIcon={value?.Icon}
|
|
||||||
text={value?.label ?? placeholder ?? ''}
|
|
||||||
accent={value?.label ? 'default' : 'placeholder'}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
dropdownComponents={
|
|
||||||
<>
|
|
||||||
<DropdownMenuSearchInput
|
|
||||||
value={searchFilter}
|
|
||||||
onChange={handleFilterChange}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
|
||||||
{options?.map((option) => {
|
|
||||||
const id = `${v4()}-${option.value}`;
|
|
||||||
return (
|
|
||||||
<React.Fragment key={id}>
|
|
||||||
<div id={id}>
|
|
||||||
<MenuItemSelect
|
|
||||||
selected={value?.label === option.label}
|
|
||||||
onClick={() => handleChange(option)}
|
|
||||||
disabled={
|
|
||||||
option.disabled && value?.value !== option.value
|
|
||||||
}
|
|
||||||
LeftIcon={option?.Icon}
|
|
||||||
text={option.label}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{option.disabled &&
|
|
||||||
value?.value !== option.value &&
|
|
||||||
createPortal(
|
|
||||||
<AppTooltip
|
|
||||||
key={id}
|
|
||||||
anchorSelect={`#${id}`}
|
|
||||||
content={t`You are already importing this column.`}
|
|
||||||
place="right"
|
|
||||||
offset={-20}
|
|
||||||
/>,
|
|
||||||
document.body,
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{options?.length === 0 && (
|
|
||||||
<MenuItem key="No results" text={t`No results`} />
|
|
||||||
)}
|
|
||||||
</DropdownMenuItemsContainer>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField';
|
||||||
|
import { DO_NOT_IMPORT_OPTION_KEY } from '@/spreadsheet-import/constants/DoNotImportOptionKey';
|
||||||
|
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||||
|
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||||
|
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||||
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
|
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||||
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { IconForbid, IconX, useIcons } from 'twenty-ui/display';
|
||||||
|
import { SelectOption } from 'twenty-ui/input';
|
||||||
|
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||||
|
import { ReadonlyDeep } from 'type-fest';
|
||||||
|
|
||||||
|
export const MatchColumnSelectFieldSelectDropdownContent = ({
|
||||||
|
selectedValue,
|
||||||
|
onSelectFieldMetadataItem,
|
||||||
|
onCancelSelect,
|
||||||
|
onDoNotImportSelect,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
selectedValue: SelectOption | undefined;
|
||||||
|
onSelectFieldMetadataItem: (
|
||||||
|
selectedFieldMetadataItem: FieldMetadataItem,
|
||||||
|
) => void;
|
||||||
|
onCancelSelect: () => void;
|
||||||
|
onDoNotImportSelect: () => void;
|
||||||
|
options: readonly ReadonlyDeep<SelectOption>[];
|
||||||
|
}) => {
|
||||||
|
const [searchFilter, setSearchFilter] = useState('');
|
||||||
|
|
||||||
|
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.currentTarget.value;
|
||||||
|
|
||||||
|
setSearchFilter(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { availableFieldMetadataItems } = useSpreadsheetImportInternal();
|
||||||
|
|
||||||
|
const filteredAvailableFieldMetadataItems =
|
||||||
|
availableFieldMetadataItems.filter(
|
||||||
|
(field) =>
|
||||||
|
field.label.toLowerCase().includes(searchFilter.toLowerCase()) ||
|
||||||
|
field.name.toLowerCase().includes(searchFilter.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
|
const handleFieldClick = (fieldMetadataItem: FieldMetadataItem) => {
|
||||||
|
onSelectFieldMetadataItem(fieldMetadataItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelClick = () => {
|
||||||
|
onCancelSelect();
|
||||||
|
};
|
||||||
|
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DropdownMenuHeader
|
||||||
|
StartComponent={
|
||||||
|
<DropdownMenuHeaderLeftComponent
|
||||||
|
onClick={handleCancelClick}
|
||||||
|
Icon={IconX}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Select matching field
|
||||||
|
</DropdownMenuHeader>
|
||||||
|
<DropdownMenuSearchInput
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItemsContainer hasMaxHeight width={200}>
|
||||||
|
<MenuItemSelect
|
||||||
|
selected={selectedValue?.value === DO_NOT_IMPORT_OPTION_KEY}
|
||||||
|
onClick={onDoNotImportSelect}
|
||||||
|
LeftIcon={IconForbid}
|
||||||
|
text={t`Do not import`}
|
||||||
|
/>
|
||||||
|
{filteredAvailableFieldMetadataItems.map((field) => (
|
||||||
|
<MenuItemSelect
|
||||||
|
key={field.id}
|
||||||
|
selected={selectedValue?.value === field.name}
|
||||||
|
onClick={() => handleFieldClick(field)}
|
||||||
|
disabled={
|
||||||
|
options.find((option) => option.value === field.name)?.disabled &&
|
||||||
|
selectedValue?.value !== field.name
|
||||||
|
}
|
||||||
|
LeftIcon={getIcon(field.icon)}
|
||||||
|
text={field.label}
|
||||||
|
hasSubMenu={isCompositeField(field.type)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField';
|
||||||
|
import { getSubFieldOptionKey } from '@/object-record/spreadsheet-import/utils/getSubFieldOptionKey';
|
||||||
|
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
||||||
|
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||||
|
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||||
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
|
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||||
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
|
import { useState } from 'react';
|
||||||
|
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,
|
||||||
|
onSubFieldSelect,
|
||||||
|
options,
|
||||||
|
onBack,
|
||||||
|
}: {
|
||||||
|
fieldMetadataItem: FieldMetadataItem;
|
||||||
|
onSubFieldSelect: (subFieldNameSelected: string) => void;
|
||||||
|
options: readonly ReadonlyDeep<SelectOption>[];
|
||||||
|
onBack: () => void;
|
||||||
|
}) => {
|
||||||
|
const [searchFilter, setSearchFilter] = useState('');
|
||||||
|
|
||||||
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
|
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.currentTarget.value;
|
||||||
|
|
||||||
|
setSearchFilter(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubFieldSelect = (subFieldName: string) => {
|
||||||
|
onSubFieldSelect(subFieldName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubMenuBack = () => {
|
||||||
|
setSearchFilter('');
|
||||||
|
onBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isCompositeField(fieldMetadataItem.type)) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldMetadataItemSettings =
|
||||||
|
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldMetadataItem.type];
|
||||||
|
|
||||||
|
const subFieldNamesThatExistInOptions = fieldMetadataItemSettings.subFields
|
||||||
|
.filter((subFieldName) => {
|
||||||
|
const optionKey = getSubFieldOptionKey(fieldMetadataItem, subFieldName);
|
||||||
|
|
||||||
|
const correspondingOption = options.find(
|
||||||
|
(option) => option.value === optionKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
return isDefined(correspondingOption);
|
||||||
|
})
|
||||||
|
.filter((subFieldName) => subFieldName.includes(searchFilter));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DropdownMenuHeader
|
||||||
|
StartComponent={
|
||||||
|
<DropdownMenuHeaderLeftComponent
|
||||||
|
onClick={handleSubMenuBack}
|
||||||
|
Icon={IconChevronLeft}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<OverflowingTextWithTooltip text={fieldMetadataItem.label} />
|
||||||
|
</DropdownMenuHeader>
|
||||||
|
<DropdownMenuSearchInput
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItemsContainer hasMaxHeight width={200}>
|
||||||
|
{subFieldNamesThatExistInOptions.map((subFieldName) => (
|
||||||
|
<MenuItem
|
||||||
|
key={subFieldName}
|
||||||
|
onClick={() => handleSubFieldSelect(subFieldName)}
|
||||||
|
LeftIcon={getIcon(fieldMetadataItem.icon)}
|
||||||
|
text={
|
||||||
|
(fieldMetadataItemSettings.labelBySubField as any)[subFieldName]
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
options.find(
|
||||||
|
(option) =>
|
||||||
|
option.value ===
|
||||||
|
getSubFieldOptionKey(fieldMetadataItem, subFieldName),
|
||||||
|
)?.disabled
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,144 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { ReadonlyDeep } from 'type-fest';
|
||||||
|
|
||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField';
|
||||||
|
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 { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { SelectOption } from 'twenty-ui/input';
|
||||||
|
import { MenuItem } from 'twenty-ui/navigation';
|
||||||
|
|
||||||
|
interface MatchColumnToFieldSelectProps {
|
||||||
|
columnIndex: string;
|
||||||
|
onChange: (value: ReadonlyDeep<SelectOption> | null) => void;
|
||||||
|
value?: ReadonlyDeep<SelectOption>;
|
||||||
|
options: readonly ReadonlyDeep<SelectOption>[];
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MatchColumnToFieldSelect = ({
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
placeholder,
|
||||||
|
columnIndex,
|
||||||
|
}: MatchColumnToFieldSelectProps) => {
|
||||||
|
const dropdownId = `match-column-select-v2-dropdown-${columnIndex}`;
|
||||||
|
|
||||||
|
const { closeDropdown } = useDropdown(dropdownId);
|
||||||
|
|
||||||
|
const [selectedFieldMetadataItem, setSelectedFieldMetadataItem] =
|
||||||
|
useState<FieldMetadataItem | null>(null);
|
||||||
|
|
||||||
|
const handleFieldMetadataItemSelect = (
|
||||||
|
selectedFieldMetadataItem: FieldMetadataItem,
|
||||||
|
) => {
|
||||||
|
setSelectedFieldMetadataItem(selectedFieldMetadataItem);
|
||||||
|
|
||||||
|
if (!isCompositeField(selectedFieldMetadataItem.type)) {
|
||||||
|
const correspondingOption = options.find(
|
||||||
|
(option) => option.value === selectedFieldMetadataItem.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDefined(correspondingOption)) {
|
||||||
|
setSelectedFieldMetadataItem(null);
|
||||||
|
|
||||||
|
onChange(correspondingOption);
|
||||||
|
closeDropdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubFieldSelect = (subFieldNameSelected: string) => {
|
||||||
|
if (!isDefined(selectedFieldMetadataItem)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const correspondingOption = options.find((option) => {
|
||||||
|
const optionKey = getSubFieldOptionKey(
|
||||||
|
selectedFieldMetadataItem,
|
||||||
|
subFieldNameSelected,
|
||||||
|
);
|
||||||
|
|
||||||
|
return option.value === optionKey;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDefined(correspondingOption)) {
|
||||||
|
setSelectedFieldMetadataItem(null);
|
||||||
|
|
||||||
|
onChange(correspondingOption);
|
||||||
|
closeDropdown();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDoNotImportSelect = () => {
|
||||||
|
if (isDefined(doNotImportOption)) {
|
||||||
|
onChange(doNotImportOption);
|
||||||
|
closeDropdown();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickOutside = () => {
|
||||||
|
setSelectedFieldMetadataItem(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubFieldBack = () => {
|
||||||
|
setSelectedFieldMetadataItem(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelSelectClick = () => {
|
||||||
|
setSelectedFieldMetadataItem(null);
|
||||||
|
closeDropdown();
|
||||||
|
};
|
||||||
|
|
||||||
|
const doNotImportOption = options.find(
|
||||||
|
(option) => option.value === DO_NOT_IMPORT_OPTION_KEY,
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldDisplaySubFieldMetadataItemSelect = isDefined(
|
||||||
|
selectedFieldMetadataItem?.type,
|
||||||
|
)
|
||||||
|
? isCompositeField(selectedFieldMetadataItem?.type)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
dropdownId={dropdownId}
|
||||||
|
dropdownHotkeyScope={{
|
||||||
|
scope: dropdownId,
|
||||||
|
}}
|
||||||
|
dropdownPlacement="bottom-start"
|
||||||
|
clickableComponent={
|
||||||
|
<MenuItem
|
||||||
|
LeftIcon={value?.Icon}
|
||||||
|
text={value?.label ?? placeholder ?? ''}
|
||||||
|
accent={value?.label ? 'default' : 'placeholder'}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
dropdownComponents={
|
||||||
|
shouldDisplaySubFieldMetadataItemSelect && selectedFieldMetadataItem ? (
|
||||||
|
<MatchColumnSelectSubFieldSelectDropdownContent
|
||||||
|
fieldMetadataItem={selectedFieldMetadataItem}
|
||||||
|
onSubFieldSelect={handleSubFieldSelect}
|
||||||
|
options={options}
|
||||||
|
onBack={handleSubFieldBack}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<MatchColumnSelectFieldSelectDropdownContent
|
||||||
|
selectedValue={value}
|
||||||
|
onSelectFieldMetadataItem={handleFieldMetadataItemSelect}
|
||||||
|
onCancelSelect={handleCancelSelectClick}
|
||||||
|
onDoNotImportSelect={handleDoNotImportSelect}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClickOutside={handleClickOutside}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -5,8 +5,8 @@ import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpre
|
|||||||
import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager';
|
import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager';
|
||||||
import { useStepBar } from '@/ui/navigation/step-bar/hooks/useStepBar';
|
import { useStepBar } from '@/ui/navigation/step-bar/hooks/useStepBar';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { IconButton } from 'twenty-ui/input';
|
|
||||||
import { IconX } from 'twenty-ui/display';
|
import { IconX } from 'twenty-ui/display';
|
||||||
|
import { IconButton } from 'twenty-ui/input';
|
||||||
|
|
||||||
const StyledCloseButtonContainer = styled.div`
|
const StyledCloseButtonContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export const DO_NOT_IMPORT_OPTION_KEY = 'do-not-import';
|
||||||
@ -45,6 +45,7 @@ export const mockedSpreadsheetOptions: SpreadsheetImportDialogOptions<Spreadshee
|
|||||||
parseRaw: true,
|
parseRaw: true,
|
||||||
rtl: false,
|
rtl: false,
|
||||||
selectHeader: true,
|
selectHeader: true,
|
||||||
|
availableFieldMetadataItems: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('useSpreadsheetImport', () => {
|
describe('useSpreadsheetImport', () => {
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|||||||
|
|
||||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||||
|
|
||||||
|
import { DO_NOT_IMPORT_OPTION_KEY } from '@/spreadsheet-import/constants/DoNotImportOptionKey';
|
||||||
import { UnmatchColumn } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumn';
|
import { UnmatchColumn } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumn';
|
||||||
import { initialComputedColumnsSelector } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/states/initialComputedColumnsState';
|
import { initialComputedColumnsSelector } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/states/initialComputedColumnsState';
|
||||||
import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep';
|
import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep';
|
||||||
@ -116,7 +117,7 @@ export const MatchColumnsStep = <T extends string>({
|
|||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(value: T, columnIndex: number) => {
|
(value: T, columnIndex: number) => {
|
||||||
if (value === 'do-not-import') {
|
if (value === DO_NOT_IMPORT_OPTION_KEY) {
|
||||||
if (columns[columnIndex].type === SpreadsheetColumnType.ignored) {
|
if (columns[columnIndex].type === SpreadsheetColumnType.ignored) {
|
||||||
onRevertIgnore(columnIndex);
|
onRevertIgnore(columnIndex);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,130 +0,0 @@
|
|||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
|
||||||
|
|
||||||
import { getFieldOptions } from '@/spreadsheet-import/utils/getFieldOptions';
|
|
||||||
|
|
||||||
import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope';
|
|
||||||
import {
|
|
||||||
SpreadsheetMatchedSelectColumn,
|
|
||||||
SpreadsheetMatchedSelectOptionsColumn,
|
|
||||||
} from '@/spreadsheet-import/types/SpreadsheetColumn';
|
|
||||||
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
|
|
||||||
import { SelectInput } from '@/ui/input/components/SelectInput';
|
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Tag, TagColor } from 'twenty-ui/components';
|
|
||||||
import { IconChevronDown } from 'twenty-ui/display';
|
|
||||||
import { SelectOption } from 'twenty-ui/input';
|
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
gap: ${({ theme }) => theme.spacing(4)};
|
|
||||||
justify-content: space-between;
|
|
||||||
padding-bottom: ${({ theme }) => theme.spacing(1)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledControlContainer = styled.div<{ cursor: string }>`
|
|
||||||
align-items: center;
|
|
||||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
|
||||||
cursor: ${({ cursor }) => cursor};
|
|
||||||
display: flex;
|
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
|
||||||
height: ${({ theme }) => theme.spacing(8)};
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledLabel = styled.span`
|
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
|
||||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
|
||||||
font-size: ${({ theme }) => theme.font.size.md};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledControlLabel = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledIconChevronDown = styled(IconChevronDown)`
|
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface SubMatchingSelectProps<T> {
|
|
||||||
option: SpreadsheetMatchedOptions<T> | Partial<SpreadsheetMatchedOptions<T>>;
|
|
||||||
column:
|
|
||||||
| SpreadsheetMatchedSelectColumn<T>
|
|
||||||
| SpreadsheetMatchedSelectOptionsColumn<T>;
|
|
||||||
onSubChange: (val: T, index: number, option: string) => void;
|
|
||||||
placeholder: string;
|
|
||||||
selectedOption?:
|
|
||||||
| SpreadsheetMatchedOptions<T>
|
|
||||||
| Partial<SpreadsheetMatchedOptions<T>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SubMatchingSelect = <T extends string>({
|
|
||||||
option,
|
|
||||||
column,
|
|
||||||
onSubChange,
|
|
||||||
placeholder,
|
|
||||||
}: SubMatchingSelectProps<T>) => {
|
|
||||||
const { fields } = useSpreadsheetImportInternal<T>();
|
|
||||||
const options = getFieldOptions(fields, column.value) as SelectOption[];
|
|
||||||
const value = options.find((opt) => opt.value === option.value);
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const handleSelect = (selectedOption: SelectOption) => {
|
|
||||||
onSubChange(selectedOption.value as T, column.index, option.entry ?? '');
|
|
||||||
setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setHotkeyScope = useSetHotkeyScope();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setHotkeyScope(SelectFieldHotkeyScope.SelectField);
|
|
||||||
}, [setHotkeyScope]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledContainer>
|
|
||||||
<StyledControlContainer cursor="default">
|
|
||||||
<StyledControlLabel>
|
|
||||||
<StyledLabel>{option.entry}</StyledLabel>
|
|
||||||
</StyledControlLabel>
|
|
||||||
<StyledIconChevronDown
|
|
||||||
size={theme.font.size.md}
|
|
||||||
color={theme.font.color.tertiary}
|
|
||||||
/>
|
|
||||||
</StyledControlContainer>
|
|
||||||
<StyledControlContainer
|
|
||||||
cursor="pointer"
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
id="control"
|
|
||||||
>
|
|
||||||
<Tag
|
|
||||||
text={value?.label ?? placeholder}
|
|
||||||
color={value?.color as TagColor}
|
|
||||||
/>
|
|
||||||
<StyledIconChevronDown size={theme.icon.size.md} />
|
|
||||||
{isOpen && (
|
|
||||||
<SelectInput
|
|
||||||
defaultOption={value}
|
|
||||||
options={options}
|
|
||||||
onOptionSelected={handleSelect}
|
|
||||||
onCancel={() => setIsOpen(false)}
|
|
||||||
hotkeyScope={SelectFieldHotkeyScope.SelectField}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</StyledControlContainer>
|
|
||||||
</StyledContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
|
import { MatchColumnToFieldSelect } from '@/spreadsheet-import/components/MatchColumnToFieldSelect';
|
||||||
|
import { DO_NOT_IMPORT_OPTION_KEY } from '@/spreadsheet-import/constants/DoNotImportOptionKey';
|
||||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||||
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
||||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||||
@ -54,7 +55,7 @@ export const TemplateColumn = <T extends string>({
|
|||||||
const selectOptions = [
|
const selectOptions = [
|
||||||
{
|
{
|
||||||
Icon: IconForbid,
|
Icon: IconForbid,
|
||||||
value: 'do-not-import',
|
value: DO_NOT_IMPORT_OPTION_KEY,
|
||||||
label: t`Do not import`,
|
label: t`Do not import`,
|
||||||
},
|
},
|
||||||
...fieldOptions,
|
...fieldOptions,
|
||||||
@ -65,12 +66,12 @@ export const TemplateColumn = <T extends string>({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const ignoreValue = selectOptions.find(
|
const ignoreValue = selectOptions.find(
|
||||||
({ value }) => value === 'do-not-import',
|
({ value }) => value === DO_NOT_IMPORT_OPTION_KEY,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<MatchColumnSelect
|
<MatchColumnToFieldSelect
|
||||||
placeholder={t`Select column...`}
|
placeholder={t`Select column...`}
|
||||||
value={isIgnored ? ignoreValue : selectValue}
|
value={isIgnored ? ignoreValue : selectValue}
|
||||||
onChange={(value) => onChange(value?.value as T, column.index)}
|
onChange={(value) => onChange(value?.value as T, column.index)}
|
||||||
|
|||||||
@ -9,11 +9,10 @@ import {
|
|||||||
} from '@/spreadsheet-import/types';
|
} from '@/spreadsheet-import/types';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
|
||||||
import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { ImportedStructuredRowMetadata } from '../types';
|
|
||||||
import { AppTooltip } from 'twenty-ui/display';
|
import { AppTooltip } from 'twenty-ui/display';
|
||||||
import { Checkbox, CheckboxVariant, Toggle } from 'twenty-ui/input';
|
import { Checkbox, CheckboxVariant, Toggle } from 'twenty-ui/input';
|
||||||
|
import { ImportedStructuredRowMetadata } from '../types';
|
||||||
|
|
||||||
const StyledHeaderContainer = styled.div`
|
const StyledHeaderContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -61,6 +60,10 @@ const StyledDefaultContainer = styled.div`
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledSelectReadonlyValueContianer = styled.div`
|
||||||
|
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
const SELECT_COLUMN_KEY = 'select-row';
|
const SELECT_COLUMN_KEY = 'select-row';
|
||||||
|
|
||||||
export const generateColumns = <T extends string>(
|
export const generateColumns = <T extends string>(
|
||||||
@ -130,26 +133,10 @@ export const generateColumns = <T extends string>(
|
|||||||
|
|
||||||
switch (column.fieldType.type) {
|
switch (column.fieldType.type) {
|
||||||
case 'select': {
|
case 'select': {
|
||||||
const value = column.fieldType.options.find(
|
|
||||||
(option) => option.value === (row[columnKey] as string),
|
|
||||||
);
|
|
||||||
|
|
||||||
component = (
|
component = (
|
||||||
<MatchColumnSelect
|
<StyledSelectReadonlyValueContianer>
|
||||||
value={
|
{row[columnKey]}
|
||||||
value
|
</StyledSelectReadonlyValueContianer>
|
||||||
? ({
|
|
||||||
Icon: undefined,
|
|
||||||
...value,
|
|
||||||
} as const)
|
|
||||||
: value
|
|
||||||
}
|
|
||||||
onChange={(value) => {
|
|
||||||
onRowChange({ ...row, [columnKey]: value?.value }, true);
|
|
||||||
}}
|
|
||||||
options={column.fieldType.options}
|
|
||||||
columnIndex={column.key}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||||
import { SpreadsheetImportFields } from '@/spreadsheet-import/types/SpreadsheetImportFields';
|
import { SpreadsheetImportFields } from '@/spreadsheet-import/types/SpreadsheetImportFields';
|
||||||
|
import { SpreadsheetImportImportValidationResult } from '@/spreadsheet-import/types/SpreadsheetImportImportValidationResult';
|
||||||
import { ImportedRow } from '@/spreadsheet-import/types/SpreadsheetImportImportedRow';
|
import { ImportedRow } from '@/spreadsheet-import/types/SpreadsheetImportImportedRow';
|
||||||
import { ImportedStructuredRow } from '@/spreadsheet-import/types/SpreadsheetImportImportedStructuredRow';
|
import { ImportedStructuredRow } from '@/spreadsheet-import/types/SpreadsheetImportImportedStructuredRow';
|
||||||
import { SpreadsheetImportImportValidationResult } from '@/spreadsheet-import/types/SpreadsheetImportImportValidationResult';
|
|
||||||
import { SpreadsheetImportRowHook } from '@/spreadsheet-import/types/SpreadsheetImportRowHook';
|
import { SpreadsheetImportRowHook } from '@/spreadsheet-import/types/SpreadsheetImportRowHook';
|
||||||
import { SpreadsheetImportTableHook } from '@/spreadsheet-import/types/SpreadsheetImportTableHook';
|
import { SpreadsheetImportTableHook } from '@/spreadsheet-import/types/SpreadsheetImportTableHook';
|
||||||
import { SpreadsheetImportStep } from '../steps/types/SpreadsheetImportStep';
|
import { SpreadsheetImportStep } from '../steps/types/SpreadsheetImportStep';
|
||||||
@ -58,4 +59,5 @@ export type SpreadsheetImportDialogOptions<FieldNames extends string> = {
|
|||||||
rtl?: boolean;
|
rtl?: boolean;
|
||||||
// Allow header selection
|
// Allow header selection
|
||||||
selectHeader?: boolean;
|
selectHeader?: boolean;
|
||||||
|
availableFieldMetadataItems: FieldMetadataItem[];
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user