From 49ed3df4896fd821cbde584256cc4b91e537a972 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 2 Dec 2025 14:40:10 +0300 Subject: [PATCH 01/11] fix: Reports - Workspace quick filter has no search field when there are at least 12 workspaces --- src/CONST/index.ts | 2 +- .../AvatarMultiSelectListItem.tsx | 171 ++++++++++++++++++ .../AvatarMultiSelectPopup.tsx | 146 +++++++++++++++ .../FilterDropdowns/MultiSelectPopup.tsx | 27 ++- .../FilterDropdowns/UserSelectPopup.tsx | 14 +- .../SearchPageHeader/SearchFiltersBar.tsx | 38 +++- .../SelectionListWithSections/types.ts | 2 + src/pages/ReportParticipantsPage.tsx | 6 +- src/pages/RoomMembersPage.tsx | 7 +- src/styles/index.ts | 6 +- tests/unit/CategoryOptionListUtilsTest.ts | 56 +++++- tests/unit/TagsOptionsListUtilsTest.ts | 65 +++++++ 12 files changed, 514 insertions(+), 26 deletions(-) create mode 100644 src/components/Search/FilterDropdowns/AvatarMultiSelectListItem.tsx create mode 100644 src/components/Search/FilterDropdowns/AvatarMultiSelectPopup.tsx diff --git a/src/CONST/index.ts b/src/CONST/index.ts index d058e4f21f1c..58b3c65e735d 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3884,7 +3884,7 @@ const CONST = { // Character Limits FORM_CHARACTER_LIMIT: 50, STANDARD_LENGTH_LIMIT: 100, - STANDARD_LIST_ITEM_LIMIT: 8, + STANDARD_LIST_ITEM_LIMIT: 12, LEGAL_NAMES_CHARACTER_LIMIT: 150, LOGIN_CHARACTER_LIMIT: 254, CATEGORY_NAME_LIMIT: 256, diff --git a/src/components/Search/FilterDropdowns/AvatarMultiSelectListItem.tsx b/src/components/Search/FilterDropdowns/AvatarMultiSelectListItem.tsx new file mode 100644 index 000000000000..156ba46d906d --- /dev/null +++ b/src/components/Search/FilterDropdowns/AvatarMultiSelectListItem.tsx @@ -0,0 +1,171 @@ +/** + * Generic list item component for multi-select lists with avatars. + * Renders an avatar, text label, and checkbox in a consistent layout. + * Used by AvatarMultiSelectPopup to display workspace and other avatar-based items. + * Follows the same layout pattern as UserSelectionListItem but without user-specific logic. + * + * IMPORTANT: This component uses SelectionListWithSections/BaseListItem (not SelectionList/ListItem/BaseListItem). + * This is required to match the UserSelectionListItem pattern and ensure proper canSelectMultiple behavior, + * which prevents the automatic checkmark from rendering when items are selected. + */ +import React, {useCallback} from 'react'; +import type {NativeSyntheticEvent, StyleProp, TargetedEvent, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import Avatar from '@components/Avatar'; +import Icon from '@components/Icon'; +import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import BaseListItem from '@components/SelectionListWithSections/BaseListItem'; +import type {ListItem} from '@components/SelectionListWithSections/types'; +import TextWithTooltip from '@components/TextWithTooltip'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; + +type AvatarMultiSelectListItemProps = { + /** The item to render */ + item: TItem; + + /** Whether the item is focused */ + isFocused?: boolean; + + /** Whether to show tooltip on truncated text */ + showTooltip: boolean; + + /** Whether the item is disabled */ + isDisabled?: boolean | null; + + /** Callback when item is selected */ + onSelectRow: (item: TItem) => void; + + /** Callback to dismiss error */ + onDismissError?: (item: TItem) => void; + + /** Whether to prevent enter key from submitting */ + shouldPreventEnterKeySubmit?: boolean; + + /** Callback when item gains focus */ + onFocus?: (event: NativeSyntheticEvent) => void; + + /** Whether to sync focus state */ + shouldSyncFocus?: boolean; + + /** Additional styles to apply to the wrapper */ + wrapperStyle?: StyleProp; + + /** Additional styles for the pressable */ + pressableStyle?: StyleProp; + + /** Callback when checkbox is pressed */ + onCheckboxPress?: (item: TItem) => void; + + /** Whether multiple items can be selected */ + canSelectMultiple?: boolean; + + /** Component to render on the right side */ + rightHandSideComponent?: ((item: TItem, isFocused?: boolean) => React.ReactElement | null | undefined) | React.ReactElement | null; +}; + +function AvatarMultiSelectListItem({ + item, + isFocused, + showTooltip, + isDisabled, + onSelectRow, + onDismissError, + shouldPreventEnterKeySubmit, + onFocus, + shouldSyncFocus, + wrapperStyle, + pressableStyle, + onCheckboxPress, + canSelectMultiple, + rightHandSideComponent, +}: AvatarMultiSelectListItemProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + const StyleUtils = useStyleUtils(); + const icons = useMemoizedLazyExpensifyIcons(['Checkmark'] as const); + + const handleCheckboxPress = useCallback(() => { + if (onCheckboxPress) { + onCheckboxPress(item); + } else { + onSelectRow(item); + } + }, [item, onCheckboxPress, onSelectRow]); + + const icon = item.icons?.at(0); + + return ( + + + {!!icon && ( + + + + )} + + + + + + + + {!!item.isSelected && ( + + )} + + + + {!!item.rightElement && item.rightElement} + + + ); +} + +AvatarMultiSelectListItem.displayName = 'AvatarMultiSelectListItem'; + +export type {AvatarMultiSelectListItemProps}; +export default AvatarMultiSelectListItem; diff --git a/src/components/Search/FilterDropdowns/AvatarMultiSelectPopup.tsx b/src/components/Search/FilterDropdowns/AvatarMultiSelectPopup.tsx new file mode 100644 index 000000000000..10aeb36df310 --- /dev/null +++ b/src/components/Search/FilterDropdowns/AvatarMultiSelectPopup.tsx @@ -0,0 +1,146 @@ +/** + * Multi-select popup component for items with avatars (e.g., workspaces). + * Displays a searchable list of items with avatars and checkboxes. + * Used by the workspace filter in search to allow selecting multiple workspaces. + * + * IMPORTANT: This component uses SelectionListWithSections (not SelectionList) to match the UserSelectPopup pattern. + * This is required for proper integration with AvatarMultiSelectListItem and ensures consistent behavior + * with the From filter. The sections-based API is necessary for the canSelectMultiple feature to work correctly. + */ +import React, {useCallback, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import Button from '@components/Button'; +import SelectionList from '@components/SelectionListWithSections'; +import type {SectionListDataType} from '@components/SelectionListWithSections/types'; +import Text from '@components/Text'; +import useDebouncedState from '@hooks/useDebouncedState'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import type {Icon} from '@src/types/onyx/OnyxCommon'; +import AvatarMultiSelectListItem from './AvatarMultiSelectListItem'; + +type AvatarMultiSelectItem = { + text: string; + value: T; + icons: Icon[]; +}; + +type AvatarMultiSelectPopupProps = { + /** The label to show when in an overlay on mobile */ + label: string; + + /** The list of all items to show up in the list */ + items: Array>; + + /** The currently selected items */ + value: Array>; + + /** Function to call to close the overlay when changes are applied */ + closeOverlay: () => void; + + /** Function to call when changes are applied */ + onChange: (item: Array>) => void; + + /** + * Whether the search input should be displayed. + * When undefined, the search input will be shown based on CONST.STANDARD_LIST_ITEM_LIMIT (12 items). + * Set to true to always show search, or false to never show search regardless of item count. + */ + isSearchable?: boolean; + + /** Search input placeholder. Defaults to 'common.search' when not provided. */ + searchPlaceholder?: string; +}; + +function AvatarMultiSelectPopup({label, value, items, closeOverlay, onChange, isSearchable, searchPlaceholder}: AvatarMultiSelectPopupProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {isSmallScreenWidth} = useResponsiveLayout(); + const {windowHeight} = useWindowDimensions(); + const [selectedItems, setSelectedItems] = useState(value); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + + type ExtendedItem = AvatarMultiSelectItem & {keyForList: string; isSelected?: boolean}; + + const sections: Array> = useMemo(() => { + const filteredItems = isSearchable ? items.filter((item) => item.text.toLowerCase().includes(debouncedSearchTerm.toLowerCase())) : items; + + return [ + { + data: filteredItems.map((item) => ({ + ...item, + keyForList: item.value, + isSelected: !!selectedItems.find((i) => i.value === item.value), + })), + shouldShow: true, + }, + ]; + }, [items, selectedItems, isSearchable, debouncedSearchTerm]); + + const updateSelectedItems = useCallback( + (item: AvatarMultiSelectItem & {keyForList: string; isSelected?: boolean}) => { + if (item.isSelected) { + setSelectedItems(selectedItems.filter((i) => i.value !== item.keyForList)); + return; + } + + const newItem = items.find((i) => i.value === item.keyForList); + + if (newItem) { + setSelectedItems([...selectedItems, newItem]); + } + }, + [items, selectedItems], + ); + + const applyChanges = useCallback(() => { + onChange(selectedItems); + closeOverlay(); + }, [closeOverlay, onChange, selectedItems]); + + const resetChanges = useCallback(() => { + onChange([]); + closeOverlay(); + }, [closeOverlay, onChange]); + + return ( + + {isSmallScreenWidth && {label}} + + + + + + +