Skip to content

Commit

Permalink
Improve Data Importer Select Matching
Browse files Browse the repository at this point in the history
  • Loading branch information
gitstart-twenty committed Aug 9, 2024
1 parent 7e01843 commit f00c73d
Show file tree
Hide file tree
Showing 42 changed files with 1,121 additions and 641 deletions.
Original file line number Diff line number Diff line change
@@ -1,29 +1,15 @@
import styled from '@emotion/styled';
import { useRef, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';

import { useClearField } from '@/object-record/record-field/hooks/useClearField';
import { useSelectField } from '@/object-record/record-field/meta-types/hooks/useSelectField';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { SINGLE_ENTITY_SELECT_BASE_LIST } from '@/object-record/relation-picker/constants/SingleEntitySelectBaseList';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
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 { SelectOption } from '@/spreadsheet-import/types';
import { SelectInput } from '@/ui/input/components/SelectInput';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { useSelectableListStates } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListStates';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { MenuItemSelectTag } from '@/ui/navigation/menu-item/components/MenuItemSelectTag';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from '~/utils/isDefined';

const StyledRelationPickerContainer = styled.div`
left: -1px;
position: absolute;
top: -1px;
`;
import { useState } from 'react';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-ui';

export type SelectFieldInputProps = {
onSubmit?: FieldInputEvent;
Expand All @@ -36,55 +22,30 @@ export const SelectFieldInput = ({
}: SelectFieldInputProps) => {
const { persistField, fieldDefinition, fieldValue, hotkeyScope } =
useSelectField();
const { selectedItemIdState } = useSelectableListStates({
selectableListScopeId: SINGLE_ENTITY_SELECT_BASE_LIST,
});
const [selectWrapperRef, setSelectWrapperRef] =
useState<HTMLDivElement | null>(null);

const [filteredOptions, setFilteredOptions] = useState<SelectOption[]>([]);

const { handleResetSelectedPosition } = useSelectableList(
SINGLE_ENTITY_SELECT_BASE_LIST,
);
const clearField = useClearField();

const selectedItemId = useRecoilValue(selectedItemIdState);
const [searchFilter, setSearchFilter] = useState('');
const containerRef = useRef<HTMLDivElement>(null);

const selectedOption = fieldDefinition.metadata.options.find(
(option) => option.value === fieldValue,
);

const optionsToSelect =
fieldDefinition.metadata.options.filter((option) => {
return (
option.value !== fieldValue &&
option.label.toLowerCase().includes(searchFilter.toLowerCase())
);
}) || [];

const optionsInDropDown = selectedOption
? [selectedOption, ...optionsToSelect]
: optionsToSelect;

// handlers
const handleClearField = () => {
clearField();
onCancel?.();
};

useListenClickOutside({
refs: [containerRef],
callback: (event) => {
event.stopImmediatePropagation();
const handleSubmit = (option: SelectOption) => {
onSubmit?.(() => persistField(option?.value));

const weAreNotInAnHTMLInput = !(
event.target instanceof HTMLInputElement &&
event.target.tagName === 'INPUT'
);
if (weAreNotInAnHTMLInput && isDefined(onCancel)) {
onCancel();
handleResetSelectedPosition();
}
},
});
handleResetSelectedPosition();
};

useScopedHotkeys(
Key.Escape,
Expand All @@ -96,81 +57,40 @@ export const SelectFieldInput = ({
[onCancel, handleResetSelectedPosition],
);

useScopedHotkeys(
Key.Enter,
() => {
const selectedOption = optionsInDropDown.find((option) =>
option.label.toLowerCase().includes(searchFilter.toLowerCase()),
);

if (isDefined(selectedOption)) {
onSubmit?.(() => persistField(selectedOption.value));
}
handleResetSelectedPosition();
},
hotkeyScope,
);

const optionIds = [
`No ${fieldDefinition.label}`,
...optionsInDropDown.map((option) => option.value),
...filteredOptions.map((option) => option.value),
];

return (
<SelectableList
selectableListId={SINGLE_ENTITY_SELECT_BASE_LIST}
selectableItemIdArray={optionIds}
hotkeyScope={hotkeyScope}
onEnter={(itemId) => {
const option = optionsInDropDown.find(
(option) => option.value === itemId,
);
if (isDefined(option)) {
onSubmit?.(() => persistField(option.value));
handleResetSelectedPosition();
}
}}
>
<StyledRelationPickerContainer ref={containerRef}>
<DropdownMenu data-select-disable>
<DropdownMenuSearchInput
value={searchFilter}
onChange={(event) => setSearchFilter(event.currentTarget.value)}
autoFocus
/>
<DropdownMenuSeparator />

<DropdownMenuItemsContainer hasMaxHeight>
{fieldDefinition.metadata.isNullable ?? (
<MenuItemSelectTag
key={`No ${fieldDefinition.label}`}
selected={false}
text={`No ${fieldDefinition.label}`}
color="transparent"
variant="outline"
onClick={handleClearField}
isKeySelected={selectedItemId === `No ${fieldDefinition.label}`}
/>
)}

{optionsInDropDown.map((option) => {
return (
<MenuItemSelectTag
key={option.value}
selected={option.value === fieldValue}
text={option.label}
color={option.color}
onClick={() => {
onSubmit?.(() => persistField(option.value));
handleResetSelectedPosition();
}}
isKeySelected={selectedItemId === option.value}
/>
);
})}
</DropdownMenuItemsContainer>
</DropdownMenu>
</StyledRelationPickerContainer>
</SelectableList>
<div ref={setSelectWrapperRef}>
<SelectableList
selectableListId={SINGLE_ENTITY_SELECT_BASE_LIST}
selectableItemIdArray={optionIds}
hotkeyScope={hotkeyScope}
onEnter={(itemId) => {
const option = filteredOptions.find(
(option) => option.value === itemId,
);
if (isDefined(option)) {
onSubmit?.(() => persistField(option.value));
handleResetSelectedPosition();
}
}}
>
<SelectInput
parentRef={selectWrapperRef}
onOptionSelected={handleSubmit}
options={fieldDefinition.metadata.options}
onCancel={onCancel}
defaultOption={selectedOption}
onFilterChange={setFilteredOptions}
onClear={
fieldDefinition.metadata.isNullable ? handleClearField : undefined
}
clearLabel={fieldDefinition.label}
/>
</SelectableList>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,38 @@ export const useBuildAvailableFieldsForImport = () => {
),
});
});
} else if (fieldMetadataItem.type === FieldMetadataType.Select) {
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: fieldMetadataItem.label,
key: fieldMetadataItem.name,
fieldType: {
type: 'select',
options:
fieldMetadataItem.options?.map((option) => ({
label: option.label,
value: option.value,
color: option.color,
})) || [],
},
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
fieldMetadataItem.label + ' (ID)',
),
});
} else if (fieldMetadataItem.type === FieldMetadataType.Boolean) {
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: fieldMetadataItem.label,
key: fieldMetadataItem.name,
fieldType: {
type: 'checkbox',
},
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
fieldMetadataItem.label,
),
});
} else {
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { FieldValidationDefinition } from '@/spreadsheet-import/types';
import {
FieldValidationDefinition,
SpreadsheetImportFieldType,
} from '@/spreadsheet-import/types';
import { IconComponent } from 'twenty-ui';

export type AvailableFieldForImport = {
icon: IconComponent;
label: string;
key: string;
fieldType: {
type: 'input' | 'checkbox';
};
fieldType: SpreadsheetImportFieldType;
fieldValidationDefinitions?: FieldValidationDefinition[];
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const StyledTitle = styled.span`
`;

const StyledDescription = styled.span`
color: ${({ theme }) => theme.font.color.primary};
color: ${({ theme }) => theme.font.color.secondary};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.regular};
margin-top: ${({ theme }) => theme.spacing(3)};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const StyledModal = styled(Modal)`
height: 61%;
min-height: 600px;
min-width: 800px;
padding: 0;
position: relative;
width: 63%;
@media (max-width: ${MOBILE_VIEWPORT}px) {
Expand Down Expand Up @@ -42,7 +43,7 @@ export const ModalWrapper = ({
return (
<>
{isOpen && (
<StyledModal size="large" onClose={onClose} isClosable={true}>
<StyledModal size="large">
<StyledRtlLtr dir={rtl ? 'rtl' : 'ltr'}>
<ModalCloseButton onClose={onClose} />
{children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';

export const RsiContext = createContext({} as any);

type ProvidersProps<T extends string> = {
type ReactSpreadsheetImportContextProviderProps<T extends string> = {
children: React.ReactNode;
values: SpreadsheetImportDialogOptions<T>;
};

export const Providers = <T extends string>({
export const ReactSpreadsheetImportContextProvider = <T extends string>({
children,
values,
}: ProvidersProps<T>) => {
}: ReactSpreadsheetImportContextProviderProps<T>) => {
if (isUndefinedOrNull(values.fields)) {
throw new Error('Fields must be provided to spreadsheet-import');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { Modal } from '@/ui/layout/modal/components/Modal';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';

const StyledFooter = styled(Modal.Footer)`
gap: ${({ theme }) => theme.spacing(2)};
gap: ${({ theme }) => theme.spacing(2.5)};
justify-content: space-between;
padding: ${({ theme }) => theme.spacing(6)} ${({ theme }) => theme.spacing(8)};
`;

type StepNavigationButtonProps = {
Expand All @@ -23,21 +24,23 @@ export const StepNavigationButton = ({
title,
isLoading,
onBack,
}: StepNavigationButtonProps) => (
<StyledFooter>
{!isUndefinedOrNull(onBack) && (
}: StepNavigationButtonProps) => {
return (
<StyledFooter>
{!isUndefinedOrNull(onBack) && (
<MainButton
Icon={isLoading ? CircularProgressBar : undefined}
title="Back"
onClick={!isLoading ? onBack : undefined}
variant="secondary"
/>
)}
<MainButton
Icon={isLoading ? CircularProgressBar : undefined}
title="Back"
onClick={!isLoading ? onBack : undefined}
variant="secondary"
title={title}
onClick={!isLoading ? onClick : undefined}
variant="primary"
/>
)}
<MainButton
Icon={isLoading ? CircularProgressBar : undefined}
title={title}
onClick={!isLoading ? onClick : undefined}
variant="primary"
/>
</StyledFooter>
);
</StyledFooter>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { RecoilRoot, useRecoilState } from 'recoil';

import { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog';
import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState';
import { StepType } from '@/spreadsheet-import/steps/components/UploadFlow';
import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType';
import {
ImportedRow,
SpreadsheetImportDialogOptions,
Expand Down Expand Up @@ -38,7 +38,7 @@ export const mockedSpreadsheetOptions: SpreadsheetImportDialogOptions<Spreadshee
autoMapHeaders: true,
autoMapDistance: 1,
initialStepState: {
type: StepType.upload,
type: SpreadsheetImportStepType.upload,
},
dateFormat: 'MM/DD/YY',
parseRaw: true,
Expand Down
Loading

0 comments on commit f00c73d

Please sign in to comment.