# Introduction closes https://github.com/twentyhq/core-team-issues/issues/591 Same than for `twenty-shared` made in https://github.com/twentyhq/twenty/pull/11083. ## TODO - [x] Manual migrate twenty-website twenty-ui imports ## What's next: - Generate barrel and migration script factorization within own package + tests - Refactoring using preconstruct ? TimeBox - Lint circular dependencies - Lint import from barrel and forbid them ### Preconstruct We need custom rollup plugins addition, but preconstruct does not expose its rollup configuration. It might be possible to handle this using the babel overrides. But was a big tunnel. We could give it a try afterwards ! ( allowing cjs interop and stuff like that ) Stuck to vite lib app Closed related PRs: - https://github.com/twentyhq/twenty/pull/11294 - https://github.com/twentyhq/twenty/pull/11203
136 lines
4.1 KiB
TypeScript
136 lines
4.1 KiB
TypeScript
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 { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
import { Key } from 'ts-key-enum';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { MenuItemSelectTag } from 'twenty-ui/navigation';
|
|
import { SelectOption } from 'twenty-ui/input';
|
|
import { TagColor } from 'twenty-ui/components';
|
|
|
|
interface SelectInputProps {
|
|
onOptionSelected: (selectedOption: SelectOption) => void;
|
|
options: SelectOption[];
|
|
onCancel?: () => void;
|
|
defaultOption?: SelectOption;
|
|
onFilterChange?: (filteredOptions: SelectOption[]) => void;
|
|
onClear?: () => void;
|
|
clearLabel?: string;
|
|
hotkeyScope: string;
|
|
}
|
|
|
|
export const SelectInput = ({
|
|
onOptionSelected,
|
|
onClear,
|
|
clearLabel,
|
|
options,
|
|
onCancel,
|
|
defaultOption,
|
|
onFilterChange,
|
|
hotkeyScope,
|
|
}: SelectInputProps) => {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
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);
|
|
};
|
|
|
|
useEffect(() => {
|
|
onFilterChange?.(optionsInDropDown);
|
|
}, [onFilterChange, optionsInDropDown]);
|
|
|
|
useListenClickOutside({
|
|
refs: [containerRef],
|
|
callback: (event) => {
|
|
event.stopImmediatePropagation();
|
|
|
|
const weAreNotInAnHTMLInput = !(
|
|
event.target instanceof HTMLInputElement &&
|
|
event.target.tagName === 'INPUT'
|
|
);
|
|
if (weAreNotInAnHTMLInput && isDefined(onCancel)) {
|
|
onCancel();
|
|
}
|
|
},
|
|
listenerId: 'select-input',
|
|
});
|
|
|
|
useScopedHotkeys(
|
|
Key.Enter,
|
|
() => {
|
|
const selectedOption = optionsInDropDown.find((option) =>
|
|
option.label.toLowerCase().includes(searchFilter.toLowerCase()),
|
|
);
|
|
if (isDefined(selectedOption)) {
|
|
handleOptionChange(selectedOption);
|
|
}
|
|
},
|
|
hotkeyScope,
|
|
[searchFilter, optionsInDropDown],
|
|
);
|
|
|
|
return (
|
|
<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) ?? 'transparent'}
|
|
onClick={() => handleOptionChange(option)}
|
|
LeftIcon={option.Icon}
|
|
/>
|
|
);
|
|
})}
|
|
</DropdownMenuItemsContainer>
|
|
</DropdownMenu>
|
|
);
|
|
};
|