TWNTY-6135 - Improve Data Importer Select Matching (#6338)
### 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:  now: ### 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 <gitstart-twenty@users.noreply.github.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
committed by
GitHub
parent
eab202f107
commit
9898ca3e53
@ -68,7 +68,9 @@ const StyledControlLabel = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledIconChevronDown = styled(IconChevronDown)<{ disabled?: boolean }>`
|
||||
const StyledIconChevronDown = styled(IconChevronDown)<{
|
||||
disabled?: boolean;
|
||||
}>`
|
||||
color: ${({ disabled, theme }) =>
|
||||
disabled ? theme.font.color.extraLight : theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
@ -0,0 +1,180 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { SelectOption } from '@/spreadsheet-import/types';
|
||||
|
||||
import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope';
|
||||
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 { MenuItemSelectTag } from '@/ui/navigation/menu-item/components/MenuItemSelectTag';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import {
|
||||
ReferenceType,
|
||||
autoUpdate,
|
||||
flip,
|
||||
offset,
|
||||
size,
|
||||
useFloating,
|
||||
} from '@floating-ui/react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { TagColor, isDefined } from 'twenty-ui';
|
||||
|
||||
const StyledRelationPickerContainer = styled.div`
|
||||
left: -1px;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
z-index: ${({ theme }) => theme.lastLayerZIndex};
|
||||
`;
|
||||
|
||||
interface SelectInputProps {
|
||||
onOptionSelected: (selectedOption: SelectOption) => void;
|
||||
options: SelectOption[];
|
||||
onCancel?: () => void;
|
||||
defaultOption?: SelectOption;
|
||||
parentRef?: ReferenceType | null | undefined;
|
||||
onFilterChange?: (filteredOptions: SelectOption[]) => void;
|
||||
onClear?: () => void;
|
||||
clearLabel?: string;
|
||||
}
|
||||
|
||||
export const SelectInput = ({
|
||||
onOptionSelected,
|
||||
onClear,
|
||||
clearLabel,
|
||||
options,
|
||||
onCancel,
|
||||
defaultOption,
|
||||
parentRef,
|
||||
onFilterChange,
|
||||
}: SelectInputProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const theme = useTheme();
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
const [selectedOption, setSelectedOption] = useState<
|
||||
SelectOption | undefined
|
||||
>(defaultOption);
|
||||
|
||||
const optionsToSelect = useMemo(
|
||||
() =>
|
||||
options.filter((option) => {
|
||||
return (
|
||||
option.value !== selectedOption?.value &&
|
||||
option.label.toLowerCase().includes(searchFilter.toLowerCase())
|
||||
);
|
||||
}) || [],
|
||||
[options, searchFilter, selectedOption?.value],
|
||||
);
|
||||
|
||||
const optionsInDropDown = useMemo(
|
||||
() =>
|
||||
selectedOption ? [selectedOption, ...optionsToSelect] : optionsToSelect,
|
||||
[optionsToSelect, selectedOption],
|
||||
);
|
||||
|
||||
const handleOptionChange = (option: SelectOption) => {
|
||||
setSelectedOption(option);
|
||||
onOptionSelected(option);
|
||||
};
|
||||
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
elements: { reference: parentRef },
|
||||
strategy: 'absolute',
|
||||
middleware: [
|
||||
offset(() => {
|
||||
return parseInt(theme.spacing(2), 10);
|
||||
}),
|
||||
flip(),
|
||||
size(),
|
||||
],
|
||||
whileElementsMounted: autoUpdate,
|
||||
open: true,
|
||||
placement: 'bottom-start',
|
||||
});
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
useEffect(() => {
|
||||
setHotkeyScope(SelectFieldHotkeyScope.SelectField);
|
||||
}, [setHotkeyScope]);
|
||||
|
||||
useEffect(() => {
|
||||
onFilterChange?.(optionsInDropDown);
|
||||
}, [onFilterChange, optionsInDropDown]);
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [refs.floating],
|
||||
callback: (event) => {
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
const weAreNotInAnHTMLInput = !(
|
||||
event.target instanceof HTMLInputElement &&
|
||||
event.target.tagName === 'INPUT'
|
||||
);
|
||||
if (weAreNotInAnHTMLInput && isDefined(onCancel)) {
|
||||
onCancel();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Enter,
|
||||
() => {
|
||||
const selectedOption = optionsInDropDown.find((option) =>
|
||||
option.label.toLowerCase().includes(searchFilter.toLowerCase()),
|
||||
);
|
||||
if (isDefined(selectedOption)) {
|
||||
handleOptionChange(selectedOption);
|
||||
}
|
||||
},
|
||||
SelectFieldHotkeyScope.SelectField,
|
||||
[searchFilter, optionsInDropDown],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledRelationPickerContainer
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
>
|
||||
<DropdownMenu ref={containerRef} data-select-disable>
|
||||
<DropdownMenuSearchInput
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{onClear && clearLabel && (
|
||||
<MenuItemSelectTag
|
||||
key={`No ${clearLabel}`}
|
||||
selected={false}
|
||||
text={`No ${clearLabel}`}
|
||||
color="transparent"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setSelectedOption(undefined);
|
||||
onClear();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{optionsInDropDown.map((option) => {
|
||||
return (
|
||||
<MenuItemSelectTag
|
||||
key={option.value}
|
||||
selected={selectedOption?.value === option.value}
|
||||
text={option.label}
|
||||
color={option.color as TagColor}
|
||||
onClick={() => handleOptionChange(option)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
</StyledRelationPickerContainer>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user