Skip to content

Commit

Permalink
TWNTY-6135 - Improve Data Importer Select Matching (#6338)
Browse files Browse the repository at this point in the history
### Description:

- we move all logic about the unmatchedOptions to a new component called
UnmatchColumn, because as it will be a full line in the table, it was
better to update where the component will be rendered
- In the latest changes to keep the columns when we change the step to
step 3 and go back to step 2, we added a fallback state
initialComputedColumnsState that saves the columns and only reverts the
updates when we go back to step 1 or close by clicking the X button

### Refs: 

#6135

```
It was necessary to add references and floating styles to the generic component to fix the bug when the last option was open and the dropdown was being hidden in the next row of the spreadsheet table. We fixed the same problem that occurs in the companies table as well
```

we used this approach mentioned on this documentation to be able to use
the hook without calling it on each component, we are calling only once,
on the shared component
<https://floating-ui.com/docs/useFloating#elements>\
before:


![](https://assets-service.gitstart.com/25493/2c994e0f-6548-4a9e-8b22-2c6eccb73b2e.png)

now:


![](https://assets-service.gitstart.com/25493/f56fd516-7e95-4616-b1ed-c9ea5195a8ae.png)###
Demo: <https://jam.dev/c/e0e0b921-7551-4a94-ac1c-8a50c53fdb0c>

Fixes #6135

NOTES: the enter key are not working on main branch too

---------

Co-authored-by: gitstart-twenty <[email protected]>
Co-authored-by: Lucas Bordeau <[email protected]>
  • Loading branch information
3 people authored Aug 22, 2024
1 parent eab202f commit 9898ca3
Show file tree
Hide file tree
Showing 44 changed files with 1,209 additions and 657 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';

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 9898ca3

Please sign in to comment.