From b86907cf4266783f1515dc8a1607bffec2d70fab Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 26 May 2026 13:07:40 +0800 Subject: [PATCH 1/2] add back missing educational tooltip --- .../Search/FilterComponents/HasSelector.tsx | 63 +++++++++++++++++++ .../Search/FilterComponents/MultiSelect.tsx | 12 +++- .../Search/FilterComponents/index.tsx | 25 +++++--- src/libs/SearchUIUtils.ts | 4 -- src/selectors/Search.ts | 3 +- src/styles/variables.ts | 2 +- 6 files changed, 91 insertions(+), 18 deletions(-) create mode 100644 src/components/Search/FilterComponents/HasSelector.tsx diff --git a/src/components/Search/FilterComponents/HasSelector.tsx b/src/components/Search/FilterComponents/HasSelector.tsx new file mode 100644 index 000000000000..45008045b9be --- /dev/null +++ b/src/components/Search/FilterComponents/HasSelector.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import {View} from 'react-native'; +import {useProductTrainingContext} from '@components/ProductTrainingContext'; +import type {SearchFilterSelectionListProps} from '@components/Search/types'; +import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getHasOptions} from '@libs/SearchUIUtils'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {filterTypeSelector} from '@src/selectors/Search'; +import type {HasFilterValues} from '@src/types/form/SearchAdvancedFiltersForm'; +import MultiSelect from './MultiSelect'; + +type HasSelectorProps = SearchFilterSelectionListProps & { + value: HasFilterValues | undefined; + onChange: (item: HasFilterValues) => void; +}; + +function HasSelector({value = [], selectionListStyle, footer, onChange}: HasSelectorProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const [type = CONST.SEARCH.DATA_TYPES.EXPENSE] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {selector: filterTypeSelector}); + const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.HAS_FILTER_NEGATION); + + const items = getHasOptions(translate, type); + const multiSelectValues = items.filter((item) => value.includes(item.value)); + + return ( + ( + + {children} + + )} + selectionListStyle={selectionListStyle} + footer={footer} + onChange={(selectedItems) => { + if (shouldShowProductTrainingTooltip) { + hideProductTrainingTooltip(); + } + onChange(selectedItems.map((item) => item.value)); + }} + /> + ); +} + +export default HasSelector; diff --git a/src/components/Search/FilterComponents/MultiSelect.tsx b/src/components/Search/FilterComponents/MultiSelect.tsx index a9d95f113275..e878a7a1068b 100644 --- a/src/components/Search/FilterComponents/MultiSelect.tsx +++ b/src/components/Search/FilterComponents/MultiSelect.tsx @@ -1,5 +1,5 @@ import React, {useState} from 'react'; -import type {ReactNode} from 'react'; +import type {ComponentType, ReactNode} from 'react'; import {View} from 'react-native'; import ActivityIndicator from '@components/ActivityIndicator'; import type {SearchFilterSelectionListProps} from '@components/Search/types'; @@ -44,6 +44,9 @@ type MultiSelectProps = SearchFilterSelectionListProps & { /** Whether the text input should be auto-focused or not. Defaults to true. */ autoFocus?: boolean; + + /** Optional wrapper component to wrap the MultiSelectListItem */ + itemWrapper?: ComponentType<{children: ReactNode; item: ListItem}>; }; function MultiSelect({ @@ -57,6 +60,7 @@ function MultiSelect({ autoFocus = true, footer, onChange, + itemWrapper, }: MultiSelectProps) { const theme = useTheme(); const {translate} = useLocalize(); @@ -106,6 +110,10 @@ function MultiSelect({ const reasonAttributes: SkeletonSpanReasonAttributes = {context: 'MultiSelectDataLoading'}; + const ListItemComponent = itemWrapper + ? (props: any) => React.createElement(itemWrapper, {children: React.createElement(MultiSelectListItem, props), item: props.item}) + : MultiSelectListItem; + return ( ({ ) => { - return searchAdvancedFiltersForm?.type; - }; - const [type = CONST.SEARCH.DATA_TYPES.EXPENSE] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, { - selector: typeSelector, - }); + const [type = CONST.SEARCH.DATA_TYPES.EXPENSE] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {selector: filterTypeSelector}); const items = getMultiSelectFilterOptions(filterKey, type, translate); const normalizedValue = Array.isArray(value) ? value : value.split(','); const multiSelectValues = items.filter((item) => normalizedValue.includes(item.value)); @@ -225,7 +220,17 @@ function FilterComponents({ /> ); } - case CONST.SEARCH.SYNTAX_FILTER_KEYS.HAS: + case CONST.SEARCH.SYNTAX_FILTER_KEYS.HAS: { + return ( + + ); + } case CONST.SEARCH.SYNTAX_FILTER_KEYS.IS: case CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPENSE_TYPE: case CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWAL_STATUS: diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 513374624ea5..3d455914ffc6 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -5266,10 +5266,6 @@ function getSingleSelectFilterOptions(filterKey: SearchAdvancedFiltersKey, trans } function getMultiSelectFilterOptions(filterKey: SearchAdvancedFiltersKey, type: SearchDataTypes, translate: LocalizedTranslate) { - if (filterKey === FILTER_KEYS.HAS) { - return getHasOptions(translate, type); - } - if (filterKey === FILTER_KEYS.IS) { return Object.values(CONST.SEARCH.IS_VALUES).map((value) => ({text: translate(`common.${value}`), value})); } diff --git a/src/selectors/Search.ts b/src/selectors/Search.ts index 644c2e05ec9a..f2df50680180 100644 --- a/src/selectors/Search.ts +++ b/src/selectors/Search.ts @@ -3,5 +3,6 @@ import type {SearchAdvancedFiltersForm} from '@src/types/form'; const filterGroupCurrencySelector = (searchAdvancedFiltersForm: OnyxEntry) => searchAdvancedFiltersForm?.groupCurrency; const filterPolicyIDSelector = (searchAdvancedFiltersForm: OnyxEntry) => searchAdvancedFiltersForm?.policyID; +const filterTypeSelector = (searchAdvancedFiltersForm: OnyxEntry) => searchAdvancedFiltersForm?.type; -export {filterGroupCurrencySelector, filterPolicyIDSelector}; +export {filterGroupCurrencySelector, filterPolicyIDSelector, filterTypeSelector}; diff --git a/src/styles/variables.ts b/src/styles/variables.ts index a8228c855037..50ca5e8f6225 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -345,7 +345,7 @@ export default { accountSwitcherTooltipShiftHorizontal: 4, expenseReportsTypeTooltipShiftHorizontal: 10, hasFilterNegationTooltipShiftHorizontal: -16, - hasFilterNegationTooltipShiftVertical: 40, + hasFilterNegationTooltipShiftVertical: -12, hasFilterNegationTooltipMaxWidth: 260, inlineImagePreviewMinSize: 64, From 46972f4e08dbf0b7c23d0a15afdd366d51f2510a Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 26 May 2026 13:23:24 +0800 Subject: [PATCH 2/2] only dismiss when tooltip is rendered --- src/components/Search/FilterComponents/HasSelector.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Search/FilterComponents/HasSelector.tsx b/src/components/Search/FilterComponents/HasSelector.tsx index 45008045b9be..2c7d4cf01b8f 100644 --- a/src/components/Search/FilterComponents/HasSelector.tsx +++ b/src/components/Search/FilterComponents/HasSelector.tsx @@ -24,6 +24,7 @@ function HasSelector({value = [], selectionListStyle, footer, onChange}: HasSele const styles = useThemeStyles(); const [type = CONST.SEARCH.DATA_TYPES.EXPENSE] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {selector: filterTypeSelector}); const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.HAS_FILTER_NEGATION); + const shouldRenderTooltip = shouldShowProductTrainingTooltip && type === CONST.SEARCH.DATA_TYPES.EXPENSE; const items = getHasOptions(translate, type); const multiSelectValues = items.filter((item) => value.includes(item.value)); @@ -34,7 +35,7 @@ function HasSelector({value = [], selectionListStyle, footer, onChange}: HasSele value={multiSelectValues} itemWrapper={({children, item}) => ( { - if (shouldShowProductTrainingTooltip) { + if (shouldRenderTooltip) { hideProductTrainingTooltip(); } onChange(selectedItems.map((item) => item.value));