diff --git a/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx b/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx index 12ce5bd92f..f3442c676b 100644 --- a/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx @@ -14,8 +14,10 @@ import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; import { type DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset'; import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState'; +import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { t } from '@lingui/core/macro'; +import { useRecoilValue, useResetRecoilState } from 'recoil'; import { IconApps, type IconComponent, useIcons } from 'twenty-ui/display'; import { IconButton, @@ -23,6 +25,8 @@ import { type IconButtonVariant, LightIconButton, } from 'twenty-ui/input'; +import { IconPickerScrollEffect } from '../hooks/IconPickerScrollEffect'; +import { iconPickerVisibleCountState } from '../states/iconPickerVisibleCountState'; export type IconPickerProps = { disabled?: boolean; @@ -60,6 +64,14 @@ const StyledLightIconButton = styled(LightIconButton)<{ : 'transparent'}; `; +const StyledLoadingMore = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 40px; + font-size: 14px; +`; + const StyledMatrixItem = styled.div` width: 32px; height: 32px; @@ -124,7 +136,7 @@ export const IconPicker = ({ clickableComponent, dropdownWidth, dropdownOffset, - maxIconsVisible = 25, + maxIconsVisible, }: IconPickerProps) => { const [searchString, setSearchString] = useState(''); @@ -144,8 +156,38 @@ export const IconPicker = ({ const { closeDropdown } = useCloseDropdown(); + const iconPickerVisibleCount = + useRecoilValue(iconPickerVisibleCountState(dropdownId)) ?? maxIconsVisible; + + const resetIconPickerVisibleCount = useResetRecoilState( + iconPickerVisibleCountState(dropdownId), + ); + const { getIcons, getIcon } = useIcons(); const icons = getIcons(); + + const totalMatchingIconsCount = useMemo(() => { + if (!icons) return 0; + + return Object.keys(icons).filter((iconKey) => { + const iconLabel = convertIconKeyToLabel(iconKey) + .toLowerCase() + .replace('icon ', '') + .replace(/\s/g, ''); + + const searchLower = searchString.toLowerCase().trim().replace(/\s/g, ''); + + return ( + iconKey === searchLower || + iconLabel === searchLower || + iconKey.startsWith(searchLower) || + iconLabel.startsWith(searchLower) || + iconKey.includes(searchLower) || + iconLabel.includes(searchLower) + ); + }).length; + }, [icons, searchString]); + const matchingSearchIconKeys = useMemo(() => { if (icons == null) return []; const scoreIconMatch = (iconKey: string, searchString: string) => { @@ -186,9 +228,9 @@ export const IconPicker = ({ ...filteredAndSortedIconKeys.filter( (iconKey) => iconKey !== selectedIconKey, ), - ].slice(0, maxIconsVisible) - : filteredAndSortedIconKeys.slice(0, maxIconsVisible); - }, [icons, searchString, selectedIconKey, maxIconsVisible]); + ].slice(0, iconPickerVisibleCount) + : filteredAndSortedIconKeys.slice(0, iconPickerVisibleCount); + }, [icons, searchString, selectedIconKey, iconPickerVisibleCount]); const iconKeys2d = useMemo( () => arrayToChunks(matchingSearchIconKeys.slice(), 5), @@ -205,6 +247,10 @@ export const IconPicker = ({ selectableListInstanceId, ) ?? undefined; + const isLoadingMore = + iconPickerVisibleCount !== undefined && + iconPickerVisibleCount < totalMatchingIconsCount; + return (