Files
twenty/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx
Weiko acc07f5bbd Fix csv import dropdown overlay (#9262)
## Context
Fixing https://github.com/twentyhq/twenty/issues/9245 using the new
OverlayContainer component instead of custom styled component

## Test
<img width="352" alt="Screenshot 2024-12-27 at 16 52 55"
src="https://github.com/user-attachments/assets/27ebcdd1-8fe4-425b-995b-bc4e7220192c"
/>
2024-12-27 17:07:37 +01:00

175 lines
5.4 KiB
TypeScript

import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
autoUpdate,
flip,
offset,
size,
useFloating,
} from '@floating-ui/react';
import React, { useCallback, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { AppTooltip, MenuItem, MenuItemSelect } from 'twenty-ui';
import { ReadonlyDeep } from 'type-fest';
import { useDebouncedCallback } from 'use-debounce';
import { SelectOption } from '@/spreadsheet-import/types';
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 { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useUpdateEffect } from '~/hooks/useUpdateEffect';
const StyledFloatingDropdown = styled.div`
z-index: ${({ theme }) => theme.lastLayerZIndex};
`;
interface MatchColumnSelectProps {
onChange: (value: ReadonlyDeep<SelectOption> | null) => void;
value?: ReadonlyDeep<SelectOption>;
options: readonly ReadonlyDeep<SelectOption>[];
placeholder?: string;
name?: string;
}
export const MatchColumnSelect = ({
onChange,
value,
options: initialOptions,
placeholder,
}: MatchColumnSelectProps) => {
const theme = useTheme();
const dropdownContainerRef = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
const [searchFilter, setSearchFilter] = useState('');
const [options, setOptions] = useState(initialOptions);
const { refs, floatingStyles } = useFloating({
strategy: 'absolute',
middleware: [
offset(() => {
return parseInt(theme.spacing(2), 10);
}),
flip(),
size(),
],
whileElementsMounted: autoUpdate,
open: isOpen,
placement: 'bottom-start',
});
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 handleDropdownItemClick = () => {
setIsOpen(true);
};
const handleChange = (option: ReadonlyDeep<SelectOption>) => {
onChange(option);
setIsOpen(false);
};
useListenClickOutside({
refs: [dropdownContainerRef],
callback: () => {
setIsOpen(false);
},
listenerId: 'match-column-select',
});
useUpdateEffect(() => {
setOptions(initialOptions);
}, [initialOptions]);
return (
<>
<div ref={refs.setReference}>
<MenuItem
LeftIcon={value?.icon}
onClick={handleDropdownItemClick}
text={value?.label ?? placeholder ?? ''}
accent={value?.label ? 'default' : 'placeholder'}
/>
</div>
{isOpen &&
createPortal(
<StyledFloatingDropdown ref={refs.setFloating} style={floatingStyles}>
<OverlayContainer>
<DropdownMenu
data-select-disable
ref={dropdownContainerRef}
// width={refs.domReference.current?.clientWidth}
>
<DropdownMenuSearchInput
value={searchFilter}
onChange={handleFilterChange}
autoFocus
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{options?.map((option) => (
<React.Fragment key={option.label}>
<MenuItemSelect
selected={value?.label === option.label}
onClick={() => handleChange(option)}
disabled={
option.disabled && value?.value !== option.value
}
LeftIcon={option?.icon}
text={option.label}
/>
{option.disabled &&
value?.value !== option.value &&
createPortal(
<AppTooltip
key={option.value}
anchorSelect={`#${option.value}`}
content="You are already importing this column."
place="right"
offset={-20}
/>,
document.body,
)}
</React.Fragment>
))}
{options?.length === 0 && (
<MenuItem key="No result" text="No result" />
)}
</DropdownMenuItemsContainer>
</DropdownMenu>
</OverlayContainer>
</StyledFloatingDropdown>,
document.body,
)}
</>
);
};