# Introduction In this PR we've migrated `twenty-shared` from a `vite` app [libary-mode](https://vite.dev/guide/build#library-mode) to a [preconstruct](https://preconstruct.tools/) "atomic" application ( in the future would like to introduce preconstruct to handle of all our atomic dependencies such as `twenty-emails` `twenty-ui` etc it will be integrated at the monorepo's root directly, would be to invasive in the first, starting incremental via `twenty-shared`) For more information regarding the motivations please refer to nor: - https://github.com/twentyhq/core-team-issues/issues/587 - https://github.com/twentyhq/core-team-issues/issues/281#issuecomment-2630949682 close https://github.com/twentyhq/core-team-issues/issues/589 close https://github.com/twentyhq/core-team-issues/issues/590 ## How to test In order to ease the review this PR will ship all the codegen at the very end, the actual meaning full diff is `+2,411 −114` In order to migrate existing dependent packages to `twenty-shared` multi barrel new arch you need to run in local: ```sh yarn tsx packages/twenty-shared/scripts/migrateFromSingleToMultiBarrelImport.ts && \ npx nx run-many -t lint --fix -p twenty-front twenty-ui twenty-server twenty-emails twenty-shared twenty-zapier ``` Note that `migrateFromSingleToMultiBarrelImport` is idempotent, it's atm included in the PR but should not be merged. ( such as codegen will be added before merging this script will be removed ) ## Misc - related opened issue preconstruct https://github.com/preconstruct/preconstruct/issues/617 ## Closed related PR - https://github.com/twentyhq/twenty/pull/11028 - https://github.com/twentyhq/twenty/pull/10993 - https://github.com/twentyhq/twenty/pull/10960 ## Upcoming enhancement: ( in others dedicated PRs ) - 1/ refactor generate barrel to export atomic module instead of `*` - 2/ generate barrel own package with several files and tests - 3/ Migration twenty-ui the same way - 4/ Use `preconstruct` at monorepo global level ## Conclusion As always any suggestions are welcomed !
136 lines
4.0 KiB
TypeScript
136 lines
4.0 KiB
TypeScript
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 { 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 { MenuItemSelectTag, TagColor } from 'twenty-ui';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
|
|
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>
|
|
);
|
|
};
|