From d5746a1607e045bb42e1b6bab5c02e4f8c9e471b Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 18 Jun 2026 12:52:08 +0800 Subject: [PATCH 01/25] add filter negation feature --- .../AdvancedFilters/AmountFilterContent.tsx | 30 ++--- .../AdvancedFilters/CommonFilterContent.tsx | 45 ------- .../AdvancedFilters/DateFilterContent.tsx | 6 +- .../SearchAdvancedFiltersContent.tsx | 123 ++++++++++------- .../TextInputFilterContent.tsx | 14 +- .../FilterComponents/NegatableFilter.tsx | 63 +++++++++ .../Search/FilterComponents/index.tsx | 124 ++++++++++++------ .../Search/FilterDropdowns/AmountPopup.tsx | 12 +- .../Search/FilterDropdowns/CommonPopup.tsx | 27 ++-- .../AmountFilterContentPopupWrapper.tsx | 4 +- .../CommonFilterContentPopupWrapper.tsx | 29 ++-- .../DateFilterContentPopupWrapper.tsx | 4 +- .../TextInputFilterContentPopupWrapper.tsx | 10 +- .../SearchAdvancedFiltersPopup/index.tsx | 2 +- .../DatePickerFilterPopup.tsx | 14 +- .../SearchPageHeader/useSearchFiltersBar.tsx | 44 +++---- .../Search/hooks/useFilterNegatableValue.tsx | 13 ++ .../Search/hooks/useGetFilterFormValues.tsx | 26 ++++ src/components/Search/types.ts | 10 ++ src/languages/en.ts | 11 ++ src/libs/SearchQueryUtils.ts | 13 +- src/libs/SearchUIUtils.ts | 84 +++++++++--- .../AmountFilterContentPageWrapper.tsx | 4 +- .../CommonFilterContentPageWrapper.tsx | 13 +- .../DateFilterContentPageWrapper.tsx | 4 +- .../SearchAdvancedFiltersContentBase.tsx | 2 +- .../TextInputFilterContentPageWrapper.tsx | 5 +- src/styles/index.ts | 6 + src/types/form/SearchAdvancedFiltersForm.ts | 15 ++- 29 files changed, 504 insertions(+), 253 deletions(-) delete mode 100644 src/components/Search/FilterComponents/AdvancedFilters/CommonFilterContent.tsx create mode 100644 src/components/Search/FilterComponents/NegatableFilter.tsx create mode 100644 src/components/Search/hooks/useFilterNegatableValue.tsx create mode 100644 src/components/Search/hooks/useGetFilterFormValues.tsx diff --git a/src/components/Search/FilterComponents/AdvancedFilters/AmountFilterContent.tsx b/src/components/Search/FilterComponents/AdvancedFilters/AmountFilterContent.tsx index c3ccd50d502f..d4563e0f4c53 100644 --- a/src/components/Search/FilterComponents/AdvancedFilters/AmountFilterContent.tsx +++ b/src/components/Search/FilterComponents/AdvancedFilters/AmountFilterContent.tsx @@ -21,7 +21,7 @@ import type {SearchAdvancedFiltersForm} from '@src/types/form'; const BETWEEN_MODIFIER = 'Between'; type AmountFilterContentProps = { - filterKey: SearchAmountFilterKeys; + baseFilterKey: SearchAmountFilterKeys; value: SearchAmountValues; largeButton?: boolean; autoFocus?: boolean; @@ -36,7 +36,7 @@ type AmountFilterHandle = { type AmountInputProps = { ref: React.Ref; - filterKey: SearchAmountFilterKeys; + baseFilterKey: SearchAmountFilterKeys; modifier: ValueOf; value: string; label: string; @@ -51,7 +51,7 @@ function getFrontendAmount(amount: string | undefined) { return amount ? convertToFrontendAmountAsString(Number(amount), CONST.DEFAULT_CURRENCY_DECIMALS) : ''; } -function AmountInput({ref, filterKey, modifier, value, label, autoFocus}: AmountInputProps) { +function AmountInput({ref, baseFilterKey, modifier, value, label, autoFocus}: AmountInputProps) { const styles = useThemeStyles(); const [amount, setAmount] = useState(value); const {didScreenTransitionEnd} = useScreenWrapperTransitionStatus(); @@ -59,7 +59,7 @@ function AmountInput({ref, filterKey, modifier, value, label, autoFocus}: Amount useImperativeHandle(ref, () => ({ getValue: () => { - const key = `${filterKey}${modifier}`; + const key = `${baseFilterKey}${modifier}`; return {[key]: getBackendAmount(amount)}; }, })); @@ -88,13 +88,13 @@ function AmountInput({ref, filterKey, modifier, value, label, autoFocus}: Amount type AmountBetweenInputProps = { ref: React.Ref; - filterKey: SearchAmountFilterKeys; + baseFilterKey: SearchAmountFilterKeys; greaterThanValue: string; lessThanValue: string; autoFocus?: boolean; }; -function AmountBetweenInput({ref, filterKey, greaterThanValue, lessThanValue, autoFocus}: AmountBetweenInputProps) { +function AmountBetweenInput({ref, baseFilterKey, greaterThanValue, lessThanValue, autoFocus}: AmountBetweenInputProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [greaterThanAmount, setGreaterThanAmount] = useState(greaterThanValue); @@ -104,8 +104,8 @@ function AmountBetweenInput({ref, filterKey, greaterThanValue, lessThanValue, au useImperativeHandle(ref, () => ({ getValue: () => { - const greaterThanKey = `${filterKey}${CONST.SEARCH.AMOUNT_MODIFIERS.GREATER_THAN}`; - const lessThanKey = `${filterKey}${CONST.SEARCH.AMOUNT_MODIFIERS.LESS_THAN}`; + const greaterThanKey = `${baseFilterKey}${CONST.SEARCH.AMOUNT_MODIFIERS.GREATER_THAN}`; + const lessThanKey = `${baseFilterKey}${CONST.SEARCH.AMOUNT_MODIFIERS.LESS_THAN}`; return {[greaterThanKey]: getBackendAmount(greaterThanAmount), [lessThanKey]: getBackendAmount(lessThanAmount)}; }, })); @@ -147,7 +147,7 @@ function AmountBetweenInput({ref, filterKey, greaterThanValue, lessThanValue, au ); } -function AmountFilterContent({filterKey, value, autoFocus, largeButton, style, onChange}: AmountFilterContentProps) { +function AmountFilterContent({baseFilterKey, value, autoFocus, largeButton, style, onChange}: AmountFilterContentProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -179,9 +179,9 @@ function AmountFilterContent({filterKey, value, autoFocus, largeButton, style, o } const formValues: AmountFilterValues = {}; - formValues[`${filterKey}${CONST.SEARCH.AMOUNT_MODIFIERS.EQUAL_TO}`] = undefined; - formValues[`${filterKey}${CONST.SEARCH.AMOUNT_MODIFIERS.GREATER_THAN}`] = undefined; - formValues[`${filterKey}${CONST.SEARCH.AMOUNT_MODIFIERS.LESS_THAN}`] = undefined; + formValues[`${baseFilterKey}${CONST.SEARCH.AMOUNT_MODIFIERS.EQUAL_TO}`] = undefined; + formValues[`${baseFilterKey}${CONST.SEARCH.AMOUNT_MODIFIERS.GREATER_THAN}`] = undefined; + formValues[`${baseFilterKey}${CONST.SEARCH.AMOUNT_MODIFIERS.LESS_THAN}`] = undefined; onChange({ ...formValues, ...inputRef.current.getValue(), @@ -198,7 +198,7 @@ function AmountFilterContent({filterKey, value, autoFocus, largeButton, style, o {keyForList: CONST.SEARCH.AMOUNT_MODIFIERS.LESS_THAN, text: translate('search.filters.amount.lessThan'), isSelected: selectedModifier === CONST.SEARCH.AMOUNT_MODIFIERS.LESS_THAN}, {keyForList: BETWEEN_MODIFIER, text: translate('search.filters.amount.between'), isSelected: selectedModifier === BETWEEN_MODIFIER}, ]; - const label = translate(FILTER_VIEW_MAP[filterKey].labelKey); + const label = translate(FILTER_VIEW_MAP[baseFilterKey].labelKey); return ( @@ -220,7 +220,7 @@ function AmountFilterContent({filterKey, value, autoFocus, largeButton, style, o (config.keyForList === BETWEEN_MODIFIER ? ( & { - filterKey: FilterComponentsProps['filterKey']; - type: SearchDataTypes | undefined; - policyIDs: string[] | undefined; - policyIDQuery: string[] | undefined; -}; - -function CommonFilterContent({ - filterKey, - value, - type, - ready, - policyIDs, - policyIDQuery, - autoFocus, - selectionListTextInputStyle, - selectionListStyle, - footer, - onChange, -}: CommonFilterContentProps) { - return ( - - ); -} - -export default CommonFilterContent; -export type {CommonFilterContentProps}; diff --git a/src/components/Search/FilterComponents/AdvancedFilters/DateFilterContent.tsx b/src/components/Search/FilterComponents/AdvancedFilters/DateFilterContent.tsx index 9ff77d96c52f..fca8c01ecf59 100644 --- a/src/components/Search/FilterComponents/AdvancedFilters/DateFilterContent.tsx +++ b/src/components/Search/FilterComponents/AdvancedFilters/DateFilterContent.tsx @@ -13,7 +13,7 @@ import {getDatePresets} from '@libs/SearchUIUtils'; import type {SearchDateModifier} from '@libs/SearchUIUtils'; type DateFilterContentProps = { - filterKey: SearchDateFilterKeys; + baseFilterKey: SearchDateFilterKeys; value: SearchDateValues; selectedDateModifier: SearchDateModifier | null; hasFeed: boolean; @@ -23,7 +23,7 @@ type DateFilterContentProps = { onChange: (values: SearchDateValues) => void; }; -function DateFilterContent({filterKey, value, selectedDateModifier, hasFeed, largeButton, style, onDateModifierSelected, onChange}: DateFilterContentProps) { +function DateFilterContent({baseFilterKey, value, selectedDateModifier, hasFeed, largeButton, style, onDateModifierSelected, onChange}: DateFilterContentProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const dateFilterRef = useRef(null); @@ -50,7 +50,7 @@ function DateFilterContent({filterKey, value, selectedDateModifier, hasFeed, lar selectedDateModifier={selectedDateModifier} onSelectDateModifier={onDateModifierSelected} defaultDateValues={value} - presets={getDatePresets(filterKey, hasFeed)} + presets={getDatePresets(baseFilterKey, hasFeed)} onSubmit={onChange} shouldShowActionButtons={false} /> diff --git a/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx b/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx index d4365dbb5518..29beec5e28b5 100644 --- a/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx +++ b/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx @@ -1,22 +1,26 @@ import React from 'react'; +import useFilterNegatableValue from '@components/Search/hooks/useFilterNegatableValue'; +import useGetFilterFormValues from '@components/Search/hooks/useGetFilterFormValues'; import {isAmountFilterKey, isDateFilterKey} from '@libs/SearchUIUtils'; import type {SearchFilter} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import type {SearchAdvancedFiltersForm} from '@src/types/form'; import type {FilterComponentsProps} from '..'; import type {AmountFilterContentProps} from './AmountFilterContent'; -import type {CommonFilterContentProps} from './CommonFilterContent'; import type {DateFilterContentProps} from './DateFilterContent'; import type {ReportFieldFilterContentProps} from './ReportFieldFilterContent'; import type {TextInputFilterContentProps} from './TextInputFilterContent'; -type TextInputFilterContentWrapperProps = Pick; -type AmountFilterContentWrapperProps = Pick; -type DateFilterContentWrapperProps = Pick; +type TextInputFilterContentWrapperProps = Pick; +type AmountFilterContentWrapperProps = Pick; +type DateFilterContentWrapperProps = Pick; type ReportFieldFilterContentWrapperProps = Pick; -type CommonFilterContentWrapperProps = Omit; +type CommonFilterContentWrapperProps = Omit & { + onChange: (value: FilterComponentsProps['value'], isNegated: boolean) => void; +}; + type SearchAdvancedFiltersContentProps = { - filterKey: SearchFilter['key']; + baseFilterKey: SearchFilter['key']; values: Partial | undefined; policyIDQuery: string[] | undefined; ready?: boolean; @@ -30,49 +34,73 @@ type SearchAdvancedFiltersContentProps = { onChange: (values: Partial) => void; }; -function getFilterFormValue(filterKey: K, value: SearchAdvancedFiltersForm[K] | undefined): Partial { - const update: Partial = {}; - update[filterKey] = value; - return update; -} +type SingleAdvancedFiltersContentProps = Pick & { + baseFilterKey: FilterComponentsProps['baseFilterKey']; + components: { + Text: React.ComponentType; + Common: React.ComponentType; + }; +}; -function SearchAdvancedFiltersContent({filterKey, values, policyIDQuery, ready, components, onChange}: SearchAdvancedFiltersContentProps) { - const {Text: TextFilter, Amount: AmountFilter, Date: DateFilter, ReportField: ReportFieldFilter, Common: CommonFilter} = components; +function SingleAdvancedFiltersContent({baseFilterKey, values, policyIDQuery, ready, components, onChange}: SingleAdvancedFiltersContentProps) { + const {isNegated, value} = useFilterNegatableValue(baseFilterKey, values); + const getFilterFormValues = useGetFilterFormValues(); if ( - filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT || - filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.DESCRIPTION || - filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_ID || - filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD || - filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.TITLE || - filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWAL_ID + baseFilterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT || + baseFilterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.DESCRIPTION || + baseFilterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_ID || + baseFilterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD || + baseFilterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.TITLE || + baseFilterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWAL_ID ) { + const TextFilter = components.Text; return ( onChange({[filterKey]: newValue})} + key={baseFilterKey} + baseFilterKey={baseFilterKey} + value={values?.[baseFilterKey]} + isNegated={isNegated} + onChange={(newValue, negated) => onChange(getFilterFormValues(baseFilterKey, newValue, negated))} /> ); } - if (isAmountFilterKey(filterKey)) { + const CommonFilter = components.Common; + return ( + onChange(getFilterFormValues(baseFilterKey, newValue, negated))} + /> + ); +} + +function SearchAdvancedFiltersContent({baseFilterKey, values, policyIDQuery, ready, components, onChange}: SearchAdvancedFiltersContentProps) { + if (isAmountFilterKey(baseFilterKey)) { + const AmountFilter = components.Amount; return ( ); } - if (isDateFilterKey(filterKey)) { + if (isDateFilterKey(baseFilterKey)) { + const DateFilter = components.Date; const onModifier = CONST.SEARCH.DATE_MODIFIERS.ON; const afterModifier = CONST.SEARCH.DATE_MODIFIERS.AFTER; const beforeModifier = CONST.SEARCH.DATE_MODIFIERS.BEFORE; @@ -80,28 +108,29 @@ function SearchAdvancedFiltersContent({filterKey, values, policyIDQuery, ready, return ( onChange({ - [`${filterKey}${onModifier}`]: newValues[onModifier], - [`${filterKey}${afterModifier}`]: newValues[afterModifier], - [`${filterKey}${beforeModifier}`]: newValues[beforeModifier], - [`${filterKey}${rangeModifier}`]: newValues[rangeModifier], + [`${baseFilterKey}${onModifier}`]: newValues[onModifier], + [`${baseFilterKey}${afterModifier}`]: newValues[afterModifier], + [`${baseFilterKey}${beforeModifier}`]: newValues[beforeModifier], + [`${baseFilterKey}${rangeModifier}`]: newValues[rangeModifier], }) } /> ); } - if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_FIELD) { + if (baseFilterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_FIELD) { + const ReportFieldFilter = components.ReportField; return ( onChange(getFilterFormValue(filterKey, newValue))} + components={{Text: components.Text, Common: components.Common}} + onChange={onChange} /> ); } diff --git a/src/components/Search/FilterComponents/AdvancedFilters/TextInputFilterContent.tsx b/src/components/Search/FilterComponents/AdvancedFilters/TextInputFilterContent.tsx index 71082b1d8ad6..d63cd2255598 100644 --- a/src/components/Search/FilterComponents/AdvancedFilters/TextInputFilterContent.tsx +++ b/src/components/Search/FilterComponents/AdvancedFilters/TextInputFilterContent.tsx @@ -8,7 +8,7 @@ import type CONST from '@src/CONST'; import FilterComponents from '..'; type TextInputFilterContentProps = { - filterKey: + baseFilterKey: | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.DESCRIPTION | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_ID @@ -16,26 +16,30 @@ type TextInputFilterContentProps = { | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.TITLE | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWAL_ID; value: string | undefined; + isNegated: boolean; largeButton?: boolean; autoFocus?: boolean; style?: StyleProp; - onChange: (value: string | undefined) => void; + onChange: (value: string | undefined, isNegated: boolean) => void; }; -function TextInputFilterContent({filterKey, value: initialValue, autoFocus, largeButton, style, onChange}: TextInputFilterContentProps) { +function TextInputFilterContent({baseFilterKey, value: initialValue, isNegated: initialIsNegated, autoFocus, largeButton, style, onChange}: TextInputFilterContentProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [value, setValue] = useState(initialValue); + const [isNegated, setIsNegated] = useState(initialIsNegated); return ( setValue(typeof v === 'string' ? v : undefined)} + onNegationChange={setIsNegated} /> {children} diff --git a/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx b/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx index f40c1d26c01d..b4019426f24c 100644 --- a/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx +++ b/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx @@ -16,7 +16,7 @@ import useOnyx from '@hooks/useOnyx'; import {close} from '@libs/actions/Modal'; import {setSearchContext} from '@libs/actions/Search'; import {getAdvancedFiltersToReset, removeNegation} from '@libs/SearchQueryUtils'; -import {FILTER_VIEW_MAP, getFilterNegatableValue, isAmountFilterKey, isDateFilterKey, isTextFilterKey, mapFiltersFormToLabelValueList, SKIPPED_SEARCH_FILTERS} from '@libs/SearchUIUtils'; +import {FILTER_VIEW_MAP, isAmountFilterKey, isDateFilterKey, isTextFilterKey, mapFiltersFormToLabelValueList, SKIPPED_SEARCH_FILTERS} from '@libs/SearchUIUtils'; import type {SearchFilter} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 4d9341846d21..8b7e126db10b 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -1,6 +1,6 @@ import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import type {FilterComponentsProps} from '@components/Search/FilterComponents'; -import {TextInputFilterContentProps} from '@components/Search/FilterComponents/AdvancedFilters/TextInputFilterContent'; +import type {TextInputFilterContentProps} from '@components/Search/FilterComponents/AdvancedFilters/TextInputFilterContent'; import type { ASTNode, Filter, @@ -2450,7 +2450,7 @@ function isFilterNegatable(key: SearchAdvancedFiltersKey) { return NEGATABLE_FILTERS.has(removeNegation(key) as SearchNegatableFilterKeys); } -function isNegated(filterKey: SearchAdvancedFiltersKey) { +function isFilterNegated(filterKey: SearchAdvancedFiltersKey) { return filterKey.endsWith(CONST.SEARCH.NOT_MODIFIER); } @@ -2516,7 +2516,7 @@ export { getParamsState, getRoutes, isSearchRootParams, - isNegated, + isFilterNegated, isFilterNegatable, removeNegation, getFilterFormValues, diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index d06e718e7723..d1323c00ba4b 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -3,7 +3,7 @@ import type {ExpensifyIconName} from '@components/Icon/ExpensifyIconLoader'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import type {MenuItemWithLink} from '@components/MenuItemList'; import type {FilterComponentsProps} from '@components/Search/FilterComponents'; -import {TextInputFilterContentProps} from '@components/Search/FilterComponents/AdvancedFilters/TextInputFilterContent'; +import type {TextInputFilterContentProps} from '@components/Search/FilterComponents/AdvancedFilters/TextInputFilterContent'; import type {MultiSelectItem} from '@components/Search/FilterComponents/MultiSelect'; import type {SingleSelectItem} from '@components/Search/FilterComponents/SingleSelect'; import type { @@ -187,7 +187,7 @@ import { getFilterFromQuery, isFilterNegatable, isFilterSupported, - isNegated, + isFilterNegated, isSearchDatePreset, removeNegation, sortOptionsWithEmptyValue, @@ -5504,14 +5504,14 @@ function getLabelValue(key: SearchAdvancedFiltersKey, labelKey: TranslationPaths } if (isFilterNegatable(key)) { - const prefix = isNegated(key) ? '-' : ''; + const prefix = isFilterNegated(key) ? '-' : ''; return `${prefix}${translate(labelKey)}`; } return translate(labelKey); } function shouldShowFilter(skipFilters: Set | undefined, key: SearchAdvancedFiltersKey, value: ValueOf, type: SearchDataTypes) { - return !skipFilters?.has(key) && (isFilterNegatable(key) || !isNegated(key)) && isFilterSupported(key, type) && value && (!Array.isArray(value) || value.length > 0); + return !skipFilters?.has(key) && (isFilterNegatable(key) || !isFilterNegated(key)) && isFilterSupported(key, type) && value && (!Array.isArray(value) || value.length > 0); } function isTextFilterKey(key: string): key is SearchTextFilterKeys { @@ -5534,15 +5534,30 @@ type SearchFilter = { type MappedFilterKey = Exclude | SearchDateFilterKeys | SearchAmountFilterKeys | typeof CONST.SEARCH.REPORT_FIELD.GLOBAL_PREFIX; -function mapFiltersFormToLabelValueList, S extends SearchAdvancedFiltersKey = never>( +function mapFiltersFormToLabelValueList( searchAdvancedFiltersForm: Partial, - skipFilters: Set | undefined, + skipFilters: Set | undefined, translate: LocalizedTranslate, localeCompare: LocaleContextProps['localeCompare'], convertToDisplayStringWithoutCurrency: CurrencyListActionsContextType['convertToDisplayStringWithoutCurrency'], - mapper?: (filterKey: Exclude) => T, -): Array { - const filters: Array = []; +): SearchFilter[]; +function mapFiltersFormToLabelValueList>( + searchAdvancedFiltersForm: Partial, + skipFilters: Set | undefined, + translate: LocalizedTranslate, + localeCompare: LocaleContextProps['localeCompare'], + convertToDisplayStringWithoutCurrency: CurrencyListActionsContextType['convertToDisplayStringWithoutCurrency'], + mapper: (filterKey: MappedFilterKey) => T, +): Array; +function mapFiltersFormToLabelValueList( + searchAdvancedFiltersForm: Partial, + skipFilters: Set | undefined, + translate: LocalizedTranslate, + localeCompare: LocaleContextProps['localeCompare'], + convertToDisplayStringWithoutCurrency: CurrencyListActionsContextType['convertToDisplayStringWithoutCurrency'], + mapper?: (filterKey: MappedFilterKey) => Record, +): SearchFilter[] { + const filters: SearchFilter[] = []; const addedGroups = new Set(); const type = searchAdvancedFiltersForm.type ?? CONST.SEARCH.DATA_TYPES.EXPENSE; @@ -5566,8 +5581,7 @@ function mapFiltersFormToLabelValueList, S ext if (displayValue && label) { addedGroups.add(syntax); - const extra = mapper?.(syntax) ?? ({} as T); - filters.push({key: syntax, label: translate(label), value: displayValue, ...extra}); + filters.push({key: syntax, label: translate(label), value: displayValue, ...mapper?.(syntax)}); } continue; } @@ -5581,7 +5595,7 @@ function mapFiltersFormToLabelValueList, S ext const value = getReportFieldDisplayValue(searchAdvancedFiltersForm, translate); if (value) { addedGroups.add(CONST.SEARCH.REPORT_FIELD.GLOBAL_PREFIX); - const extra = mapper?.(CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_FIELD as Exclude) ?? ({} as T); + const extra = mapper?.(CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_FIELD); filters.push({key: CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_FIELD, label: translate('workspace.common.reportField'), value, ...extra}); } continue; @@ -5589,14 +5603,17 @@ function mapFiltersFormToLabelValueList, S ext // Handle regular filters const baseKey = removeNegation(key); - const labelKey = baseKey in FILTER_VIEW_MAP ? FILTER_VIEW_MAP[baseKey as keyof typeof FILTER_VIEW_MAP].labelKey : undefined; + if (!hasKey(FILTER_VIEW_MAP, baseKey)) { + continue; + } + + const labelKey = FILTER_VIEW_MAP[baseKey].labelKey; const value = getDisplayValue(key, searchAdvancedFiltersForm, type, translate, localeCompare); const label = getLabelValue(key, labelKey, translate); if (label && value && !(Array.isArray(value) && value.length === 0)) { - const filterLabelMapKey = baseKey as keyof typeof FILTER_VIEW_MAP; - const extra = mapper?.(key as Exclude) ?? ({} as T); - filters.push({key: filterLabelMapKey, label, value, ...extra}); + const filterLabelMapKey = baseKey; + filters.push({key: filterLabelMapKey, label, value, ...mapper?.(key)}); } } diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index 934e6ec05ac0..7fbe66e87ab0 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -29,7 +29,7 @@ import { getQueryWithUpdatedValues, getRangeBoundariesFromFormValue, getRoutes, - isNegated, + isFilterNegated, isDefaultExpenseReportsQuery, isDefaultExpensesQuery, isSearchRootParams, @@ -3574,18 +3574,18 @@ describe('SearchQueryUtils', () => { describe('isNegated', () => { it('returns true for negated filter keys (ending with the NOT modifier)', () => { - expect(isNegated(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT}${CONST.SEARCH.NOT_MODIFIER}`)).toBe(true); - expect(isNegated(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM}${CONST.SEARCH.NOT_MODIFIER}`)).toBe(true); - expect(isNegated(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.TO}${CONST.SEARCH.NOT_MODIFIER}`)).toBe(true); - expect(isNegated(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.HAS}${CONST.SEARCH.NOT_MODIFIER}`)).toBe(true); - expect(isNegated(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY}${CONST.SEARCH.NOT_MODIFIER}`)).toBe(true); + expect(isFilterNegated(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT}${CONST.SEARCH.NOT_MODIFIER}`)).toBe(true); + expect(isFilterNegated(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM}${CONST.SEARCH.NOT_MODIFIER}`)).toBe(true); + expect(isFilterNegated(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.TO}${CONST.SEARCH.NOT_MODIFIER}`)).toBe(true); + expect(isFilterNegated(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.HAS}${CONST.SEARCH.NOT_MODIFIER}`)).toBe(true); + expect(isFilterNegated(`${CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY}${CONST.SEARCH.NOT_MODIFIER}`)).toBe(true); }); it('returns false for base (non-negated) filter keys', () => { - expect(isNegated(CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT)).toBe(false); - expect(isNegated(CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM)).toBe(false); - expect(isNegated(CONST.SEARCH.SYNTAX_FILTER_KEYS.TO)).toBe(false); - expect(isNegated(CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY)).toBe(false); + expect(isFilterNegated(CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT)).toBe(false); + expect(isFilterNegated(CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM)).toBe(false); + expect(isFilterNegated(CONST.SEARCH.SYNTAX_FILTER_KEYS.TO)).toBe(false); + expect(isFilterNegated(CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY)).toBe(false); }); }); From f797c1fd1e7250dff160085c66a667c693712848 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 14 Jul 2026 23:57:47 +0800 Subject: [PATCH 19/25] rename --- .../SearchAdvancedFiltersContent.tsx | 18 ++++++------- .../{index.tsx => ListFilterContent.tsx} | 26 +++++++++---------- .../FilterComponents/NegatableFilter.tsx | 4 +-- .../{CommonPopup.tsx => ListPopup.tsx} | 12 ++++----- ....tsx => ListFilterContentPopupWrapper.tsx} | 10 +++---- .../SearchAdvancedFiltersPopup/index.tsx | 4 +-- .../SearchPageHeader/useSearchFiltersBar.tsx | 4 +-- src/libs/SearchQueryUtils.ts | 4 +-- src/libs/SearchUIUtils.ts | 4 +-- ...r.tsx => ListFilterContentPageWrapper.tsx} | 13 +++++----- .../SearchAdvancedFiltersContentBase.tsx | 4 +-- 11 files changed, 51 insertions(+), 52 deletions(-) rename src/components/Search/FilterComponents/{index.tsx => ListFilterContent.tsx} (89%) rename src/components/Search/FilterDropdowns/{CommonPopup.tsx => ListPopup.tsx} (78%) rename src/components/Search/FilterDropdowns/SearchAdvancedFiltersPopup/{CommonFilterContentPopupWrapper.tsx => ListFilterContentPopupWrapper.tsx} (70%) rename src/pages/Search/SearchAdvancedFiltersContentPage/{CommonFilterContentPageWrapper.tsx => ListFilterContentPageWrapper.tsx} (56%) diff --git a/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx b/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx index a9197e013e8d..b8316523c4e5 100644 --- a/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx +++ b/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx @@ -9,7 +9,7 @@ import type {SearchAdvancedFiltersForm} from '@src/types/form'; import React from 'react'; -import type {FilterComponentsProps} from '..'; +import type {ListFilterContentProps} from '../ListFilterContent'; import type {AmountFilterContentProps} from './AmountFilterContent'; import type {DateFilterContentProps} from './DateFilterContent'; import type {ReportFieldFilterContentProps} from './ReportFieldFilterContent'; @@ -19,8 +19,8 @@ type TextInputFilterContentWrapperProps = Pick; type DateFilterContentWrapperProps = Pick; type ReportFieldFilterContentWrapperProps = Pick; -type CommonFilterContentWrapperProps = Omit & { - onChange: (value: FilterComponentsProps['value'], isNegated: boolean) => void; +type ListFilterContentWrapperProps = Omit & { + onChange: (value: ListFilterContentProps['value'], isNegated: boolean) => void; }; type SearchAdvancedFiltersContentProps = { @@ -32,7 +32,7 @@ type SearchAdvancedFiltersContentProps = { Amount: React.ComponentType; Date: React.ComponentType; ReportField: React.ComponentType; - Common: React.ComponentType; + List: React.ComponentType; }; onChange: (values: Partial) => void; }; @@ -41,7 +41,7 @@ type SingleAdvancedFiltersContentProps = Pick; components: { Text: React.ComponentType; - Common: React.ComponentType; + List: React.ComponentType; }; }; @@ -61,9 +61,9 @@ function SingleAdvancedFiltersContent({baseFilterKey, values, ready, components, } const {isNegated, value} = getFilterNegatableValue(baseFilterKey, values); - const CommonFilter = components.Common; + const ListFilter = components.List; return ( - ); } export default SearchAdvancedFiltersContent; -export type {TextInputFilterContentWrapperProps, AmountFilterContentWrapperProps, DateFilterContentWrapperProps, ReportFieldFilterContentWrapperProps, CommonFilterContentWrapperProps}; +export type {TextInputFilterContentWrapperProps, AmountFilterContentWrapperProps, DateFilterContentWrapperProps, ReportFieldFilterContentWrapperProps, ListFilterContentWrapperProps}; diff --git a/src/components/Search/FilterComponents/index.tsx b/src/components/Search/FilterComponents/ListFilterContent.tsx similarity index 89% rename from src/components/Search/FilterComponents/index.tsx rename to src/components/Search/FilterComponents/ListFilterContent.tsx index c216ec0035a7..6ad7e9da5568 100644 --- a/src/components/Search/FilterComponents/index.tsx +++ b/src/components/Search/FilterComponents/ListFilterContent.tsx @@ -32,7 +32,7 @@ import UserSelector from './UserSelector'; import WorkspaceSelector from './WorkspaceSelector'; type FilterKeys = Exclude; -type FilterComponentsProps = SearchFilterCommonProps & { +type ListFilterContentProps = SearchFilterCommonProps & { baseFilterKey: FilterKeys; isNegated: boolean; type?: SearchDataTypes; @@ -42,7 +42,7 @@ type FilterComponentsProps = SearchFilterCommonProps & { +type SingleSelectListFilterContentProps = SearchFilterCommonProps & { baseFilterKey: SingleSelectFilterKeys; }; @@ -54,12 +54,12 @@ type MultiSelectFilterKeys = | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWAL_STATUS | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.PAID_STATUS | typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS; -type MultiSelectFilterComponentsProps = SearchFilterCommonProps & { +type MultiSelectListFilterContentProps = SearchFilterCommonProps & { baseFilterKey: MultiSelectFilterKeys; type: SearchDataTypes | undefined; }; -function SingleSelectFilterComponents({baseFilterKey, value, selectionListStyle, footer, onChange}: SingleSelectFilterComponentsProps) { +function SingleSelectListFilterContent({baseFilterKey, value, selectionListStyle, footer, onChange}: SingleSelectListFilterContentProps) { const {translate} = useLocalize(); const items = getSingleSelectFilterOptions(baseFilterKey, translate); @@ -75,7 +75,7 @@ function SingleSelectFilterComponents({baseFilterKey, value, selectionListStyle, ); } -function MultiSelectFilterComponents({baseFilterKey, value = [], type = CONST.SEARCH.DATA_TYPES.EXPENSE, selectionListStyle, footer, onChange}: MultiSelectFilterComponentsProps) { +function MultiSelectListFilterContent({baseFilterKey, value = [], type = CONST.SEARCH.DATA_TYPES.EXPENSE, selectionListStyle, footer, onChange}: MultiSelectListFilterContentProps) { const {translate} = useLocalize(); const items = getMultiSelectFilterOptions(baseFilterKey, type, translate); const multiSelectValues = items.filter((item) => (value as string[]).includes(item.value)); @@ -93,7 +93,7 @@ function MultiSelectFilterComponents({baseFilterKey, value = [], type = CONST.SE ); } -function FilterComponents({ +function ListFilterContent({ baseFilterKey, value, isNegated, @@ -107,7 +107,7 @@ function FilterComponents({ footer, onChange, onNegationChange, -}: FilterComponentsProps) { +}: ListFilterContentProps) { const styles = useThemeStyles(); const isFilterKeyNegatable = isFilterNegatable(baseFilterKey); const selectionListTextInputStyle = [selectionListTextInputStyleProp, isFilterKeyNegatable && styles.pt2]; @@ -147,7 +147,7 @@ function FilterComponents({ break; } case CONST.SEARCH.SYNTAX_FILTER_KEYS.TYPE: { - const isTypeFilterValue = (v: FilterComponentsProps['value']): v is SearchDataTypes => { + const isTypeFilterValue = (v: ListFilterContentProps['value']): v is SearchDataTypes => { return typeof v === 'string'; }; content = ( @@ -178,12 +178,12 @@ function FilterComponents({ case CONST.SEARCH.SYNTAX_FILTER_KEYS.BILLABLE: case CONST.SEARCH.SYNTAX_FILTER_KEYS.REIMBURSABLE: case CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWAL_TYPE: { - const isSingleSelectFilterValue = (v: FilterComponentsProps['value']): v is SingleSelectFilterComponentsProps['value'] => { + const isSingleSelectFilterValue = (v: ListFilterContentProps['value']): v is SingleSelectListFilterContentProps['value'] => { return typeof v === 'string'; }; content = ( - ; onNegationChange: (isNegated: boolean) => void; }; diff --git a/src/components/Search/FilterDropdowns/CommonPopup.tsx b/src/components/Search/FilterDropdowns/ListPopup.tsx similarity index 78% rename from src/components/Search/FilterDropdowns/CommonPopup.tsx rename to src/components/Search/FilterDropdowns/ListPopup.tsx index ea99ce318fb9..b4f9c36f304d 100644 --- a/src/components/Search/FilterDropdowns/CommonPopup.tsx +++ b/src/components/Search/FilterDropdowns/ListPopup.tsx @@ -1,5 +1,5 @@ -import FilterComponents from '@components/Search/FilterComponents'; -import type {FilterComponentsProps} from '@components/Search/FilterComponents'; +import ListFilterContent from '@components/Search/FilterComponents/ListFilterContent'; +import type {ListFilterContentProps} from '@components/Search/FilterComponents/ListFilterContent'; import {getFilterFormValues} from '@libs/SearchQueryUtils'; import {getFilterNegatableValue} from '@libs/SearchUIUtils'; @@ -13,14 +13,14 @@ import type {PopoverComponentProps} from './FilterPopupButton'; import BasePopup from './BasePopup'; -type CommonPopupProps = Pick & { +type ListPopupProps = Pick & { values: Partial | undefined; label: string; closeOverlay: PopoverComponentProps['closeOverlay']; updateFilterForm: (value: Partial) => void; }; -function CommonPopup({baseFilterKey, values, label, updateFilterForm, closeOverlay}: CommonPopupProps) { +function ListPopup({baseFilterKey, values, label, updateFilterForm, closeOverlay}: ListPopupProps) { const {isNegated: initialIsNegated, value: initialValue} = getFilterNegatableValue(baseFilterKey, values); const [value, setValue] = useState(initialValue); const [isNegated, setIsNegated] = useState(initialIsNegated); @@ -36,7 +36,7 @@ function CommonPopup({baseFilterKey, values, label, updateFilterForm, closeOverl onApply={applyChanges} applySentryLabel={`Search-FilterPopupApply-${baseFilterKey}`} > - ( +function getFilterFormValues( baseFilterKey: K, value: SearchAdvancedFiltersForm[K] | undefined, isNegated: boolean, diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index d1323c00ba4b..4eb5fccd6490 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -2,8 +2,8 @@ import type {CurrencyListActionsContextType} from '@components/CurrencyListConte import type {ExpensifyIconName} from '@components/Icon/ExpensifyIconLoader'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import type {MenuItemWithLink} from '@components/MenuItemList'; -import type {FilterComponentsProps} from '@components/Search/FilterComponents'; import type {TextInputFilterContentProps} from '@components/Search/FilterComponents/AdvancedFilters/TextInputFilterContent'; +import type {ListFilterContentProps} from '@components/Search/FilterComponents/ListFilterContent'; import type {MultiSelectItem} from '@components/Search/FilterComponents/MultiSelect'; import type {SingleSelectItem} from '@components/Search/FilterComponents/SingleSelect'; import type { @@ -5483,7 +5483,7 @@ function getDisplayValue( return Array.isArray(formValue) ? formValue.join(', ') : formValue; } -function getFilterNegatableValue( +function getFilterNegatableValue( baseFilterKey: K, values: (Partial & Partial>) | undefined, ): { diff --git a/src/pages/Search/SearchAdvancedFiltersContentPage/CommonFilterContentPageWrapper.tsx b/src/pages/Search/SearchAdvancedFiltersContentPage/ListFilterContentPageWrapper.tsx similarity index 56% rename from src/pages/Search/SearchAdvancedFiltersContentPage/CommonFilterContentPageWrapper.tsx rename to src/pages/Search/SearchAdvancedFiltersContentPage/ListFilterContentPageWrapper.tsx index f5032a95f892..0548e28ca91f 100644 --- a/src/pages/Search/SearchAdvancedFiltersContentPage/CommonFilterContentPageWrapper.tsx +++ b/src/pages/Search/SearchAdvancedFiltersContentPage/ListFilterContentPageWrapper.tsx @@ -1,19 +1,18 @@ import Button from '@components/Button'; -import type {FilterComponentsProps} from '@components/Search/FilterComponents'; -import FilterComponents from '@components/Search/FilterComponents'; -import type {CommonFilterContentWrapperProps} from '@components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent'; +import type {ListFilterContentWrapperProps} from '@components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent'; +import ListFilterContent from '@components/Search/FilterComponents/ListFilterContent'; import useLocalize from '@hooks/useLocalize'; import React, {useState} from 'react'; -function CommonFilterContentPageWrapper({baseFilterKey, value: initialValue, isNegated: initialIsNegated, type, policyID, ready, onChange}: CommonFilterContentWrapperProps) { +function ListFilterContentPageWrapper({baseFilterKey, value: initialValue, isNegated: initialIsNegated, type, policyID, ready, onChange}: ListFilterContentWrapperProps) { const {translate} = useLocalize(); - const [value, setValue] = useState(initialValue); + const [value, setValue] = useState(initialValue); const [isNegated, setIsNegatedValue] = useState(initialIsNegated); return ( - Date: Wed, 15 Jul 2026 00:22:25 +0800 Subject: [PATCH 20/25] change submodule --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 8dfb01854680..f24eed5127c3 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 8dfb01854680e9327e891ad42984fe3515905962 +Subproject commit f24eed5127c3723f04877ecbd62ad2711ca09cf9 From 7bb704af89901ab0778aed64265ae74110b9cd15 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 15 Jul 2026 11:03:53 +0800 Subject: [PATCH 21/25] clear the negated status and has too --- src/components/Search/hooks/useUpdateFilterQuery.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/Search/hooks/useUpdateFilterQuery.tsx b/src/components/Search/hooks/useUpdateFilterQuery.tsx index 08dac155a641..4dff2aefe8c2 100644 --- a/src/components/Search/hooks/useUpdateFilterQuery.tsx +++ b/src/components/Search/hooks/useUpdateFilterQuery.tsx @@ -24,7 +24,9 @@ function useUpdateFilterQuery(queryJSON: SearchQueryJSON | undefined) { if (updatedFilterFormValues.type !== currentValues.type) { updatedFilterFormValues.columns = []; updatedFilterFormValues.status = undefined; + updatedFilterFormValues.statusNot = undefined; updatedFilterFormValues.has = filterValidHasValues(updatedFilterFormValues.has, updatedFilterFormValues.type, translate); + updatedFilterFormValues.hasNot = filterValidHasValues(updatedFilterFormValues.hasNot, updatedFilterFormValues.type, translate); } if (updatedFilterFormValues.groupBy !== currentValues.groupBy) { From bcf345be0ab1d65296be991b8696c8ba092796e7 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 15 Jul 2026 11:14:33 +0800 Subject: [PATCH 22/25] simplify --- .../SearchAdvancedFiltersContent.tsx | 68 +++++++------------ 1 file changed, 23 insertions(+), 45 deletions(-) diff --git a/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx b/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx index b8316523c4e5..586835b5a9e8 100644 --- a/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx +++ b/src/components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent.tsx @@ -1,4 +1,4 @@ -import type {SearchAmountFilterKeys, SearchDateFilterKeys} from '@components/Search/types'; +import type {ListFilterContentProps} from '@components/Search/FilterComponents/ListFilterContent'; import {getFilterFormValues} from '@libs/SearchQueryUtils'; import {getFilterNegatableValue, isAmountFilterKey, isDateFilterKey, isTextFilterKey} from '@libs/SearchUIUtils'; @@ -9,7 +9,6 @@ import type {SearchAdvancedFiltersForm} from '@src/types/form'; import React from 'react'; -import type {ListFilterContentProps} from '../ListFilterContent'; import type {AmountFilterContentProps} from './AmountFilterContent'; import type {DateFilterContentProps} from './DateFilterContent'; import type {ReportFieldFilterContentProps} from './ReportFieldFilterContent'; @@ -37,45 +36,6 @@ type SearchAdvancedFiltersContentProps = { onChange: (values: Partial) => void; }; -type SingleAdvancedFiltersContentProps = Pick & { - baseFilterKey: Exclude; - components: { - Text: React.ComponentType; - List: React.ComponentType; - }; -}; - -function SingleAdvancedFiltersContent({baseFilterKey, values, ready, components, onChange}: SingleAdvancedFiltersContentProps) { - if (isTextFilterKey(baseFilterKey)) { - const {isNegated, value} = getFilterNegatableValue(baseFilterKey, values); - const TextFilter = components.Text; - return ( - onChange(getFilterFormValues(baseFilterKey, newValue, negated))} - /> - ); - } - - const {isNegated, value} = getFilterNegatableValue(baseFilterKey, values); - const ListFilter = components.List; - return ( - onChange(getFilterFormValues(baseFilterKey, newValue, negated))} - /> - ); -} - function SearchAdvancedFiltersContent({baseFilterKey, values, ready, components, onChange}: SearchAdvancedFiltersContentProps) { if (isAmountFilterKey(baseFilterKey)) { const AmountFilter = components.Amount; @@ -133,14 +93,32 @@ function SearchAdvancedFiltersContent({baseFilterKey, values, ready, components, ); } + if (isTextFilterKey(baseFilterKey)) { + const {isNegated, value} = getFilterNegatableValue(baseFilterKey, values); + const TextFilter = components.Text; + return ( + onChange(getFilterFormValues(baseFilterKey, newValue, negated))} + /> + ); + } + + const {isNegated, value} = getFilterNegatableValue(baseFilterKey, values); + const ListFilter = components.List; return ( - onChange(getFilterFormValues(baseFilterKey, newValue, negated))} /> ); } From ff4270f954a01d494b8248725653a7cf008a10e9 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 15 Jul 2026 11:41:57 +0800 Subject: [PATCH 23/25] lint --- .../TextInputFilterContent.tsx | 3 +-- .../SearchPageHeader/useSearchFiltersBar.tsx | 8 ++++---- src/libs/SearchQueryUtils.ts | 11 +++++++---- src/libs/SearchUIUtils.ts | 19 ++++++++++++++----- .../ListFilterContentPageWrapper.tsx | 13 ++++++++----- 5 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/components/Search/FilterComponents/AdvancedFilters/TextInputFilterContent.tsx b/src/components/Search/FilterComponents/AdvancedFilters/TextInputFilterContent.tsx index df8bfb1b2220..7963769a44ff 100644 --- a/src/components/Search/FilterComponents/AdvancedFilters/TextInputFilterContent.tsx +++ b/src/components/Search/FilterComponents/AdvancedFilters/TextInputFilterContent.tsx @@ -1,4 +1,5 @@ import Button from '@components/Button'; +import NegatableFilter from '@components/Search/FilterComponents/NegatableFilter'; import useTextFilterValidation from '@components/Search/hooks/useTextFilterValidation'; import type {ReportFieldTextKey, SearchTextFilterKeys} from '@components/Search/types'; import TextInput from '@components/TextInput'; @@ -17,8 +18,6 @@ import type {TextInput as RNTextInput, StyleProp, ViewStyle} from 'react-native' import React, {useState} from 'react'; import {View} from 'react-native'; -import NegatableFilter from '../NegatableFilter'; - type TextInputFilterContentProps = { baseFilterKey: Exclude; value: string | undefined; diff --git a/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx b/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx index 1d87fe458010..4683a486510a 100644 --- a/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx +++ b/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx @@ -16,7 +16,7 @@ import useOnyx from '@hooks/useOnyx'; import {close} from '@libs/actions/Modal'; import {setSearchContext} from '@libs/actions/Search'; import {getAdvancedFiltersToReset, removeNegation} from '@libs/SearchQueryUtils'; -import {FILTER_VIEW_MAP, isAmountFilterKey, isDateFilterKey, isTextFilterKey, mapFiltersFormToLabelValueList, SKIPPED_SEARCH_FILTERS} from '@libs/SearchUIUtils'; +import {FILTER_VIEW_MAP, isAmountFilterKey, isDateFilterKey, isReportFieldKey, isTextFilterKey, mapFiltersFormToLabelValueList, SKIPPED_SEARCH_FILTERS} from '@libs/SearchUIUtils'; import type {SearchFilter} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; @@ -152,7 +152,7 @@ function useSearchFiltersBar(queryJSON: SearchQueryJSON): UseSearchFiltersBarRes PopoverComponent: ({closeOverlay, setPopoverWidth}) => ( { - if (curr.startsWith(CONST.SEARCH.REPORT_FIELD.GLOBAL_PREFIX)) { - acc[curr as SearchAdvancedFiltersKey] = undefined; + if (isReportFieldKey(curr)) { + acc[curr] = undefined; } return acc; }, {} as Partial); diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index accb304c0df6..94e2490766dc 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -2442,8 +2442,11 @@ function addNegation(filterKey: T, isNegated: boolean): T | `$ return isNegated ? `${filterKey}${CONST.SEARCH.NOT_MODIFIER}` : filterKey; } -function removeNegation(filterKey: string) { - return filterKey.replace(CONST.SEARCH.NOT_MODIFIER, ''); +type RemoveNegation = T extends `${infer S}${typeof CONST.SEARCH.NOT_MODIFIER}` ? S : T; + +function removeNegation(filterKey: T): RemoveNegation; +function removeNegation(filterKey: string): string { + return filterKey.endsWith(CONST.SEARCH.NOT_MODIFIER) ? filterKey.slice(0, -CONST.SEARCH.NOT_MODIFIER.length) : filterKey; } function isFilterNegatable(key: SearchAdvancedFiltersKey) { @@ -2459,8 +2462,8 @@ function getFilterFormValues { - const update: Partial = {}; - const negatedFilterKey = `${baseFilterKey}${CONST.SEARCH.NOT_MODIFIER}` as K; + const update: Partial> = {}; + const negatedFilterKey = addNegation(baseFilterKey, true); if (isFilterNegatable(baseFilterKey)) { update[negatedFilterKey] = isNegated ? value : undefined; update[baseFilterKey] = isNegated ? undefined : value; diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 4eb5fccd6490..2ea67c2afb3f 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -31,6 +31,7 @@ import {GROUP_ITEM_TYPES} from '@components/Search/SearchList/ListItem/types'; import type { GroupedItem, QueryFilters, + ReportFieldKey, ReportFieldTextKey, SearchAmountFilterKeys, SearchColumnType, @@ -5526,13 +5527,21 @@ function isDateFilterKey(key: string): key is Exclude | SearchDateFilterKeys | SearchAmountFilterKeys | typeof CONST.SEARCH.REPORT_FIELD.GLOBAL_PREFIX; +type MappedFilterKey = SearchFilter['key'] | Extract; + +function isMappedFilterKey(key: string): key is MappedFilterKey { + return hasKey(FILTER_VIEW_MAP, removeNegation(key)); +} function mapFiltersFormToLabelValueList( searchAdvancedFiltersForm: Partial, @@ -5602,18 +5611,17 @@ function mapFiltersFormToLabelValueList( } // Handle regular filters - const baseKey = removeNegation(key); - if (!hasKey(FILTER_VIEW_MAP, baseKey)) { + if (!isMappedFilterKey(key)) { continue; } + const baseKey = removeNegation(key); const labelKey = FILTER_VIEW_MAP[baseKey].labelKey; const value = getDisplayValue(key, searchAdvancedFiltersForm, type, translate, localeCompare); const label = getLabelValue(key, labelKey, translate); if (label && value && !(Array.isArray(value) && value.length === 0)) { - const filterLabelMapKey = baseKey; - filters.push({key: filterLabelMapKey, label, value, ...mapper?.(key)}); + filters.push({key: baseKey, label, value, ...mapper?.(key)}); } } @@ -6594,6 +6602,7 @@ export { isTextFilterKey, isAmountFilterKey, isDateFilterKey, + isReportFieldKey, getSingleSelectFilterOptions, getMultiSelectFilterOptions, TODO_SEARCH_KEYS, diff --git a/src/pages/Search/SearchAdvancedFiltersContentPage/ListFilterContentPageWrapper.tsx b/src/pages/Search/SearchAdvancedFiltersContentPage/ListFilterContentPageWrapper.tsx index 0548e28ca91f..fd3c3fa802b9 100644 --- a/src/pages/Search/SearchAdvancedFiltersContentPage/ListFilterContentPageWrapper.tsx +++ b/src/pages/Search/SearchAdvancedFiltersContentPage/ListFilterContentPageWrapper.tsx @@ -1,9 +1,11 @@ -import Button from '@components/Button'; +import Button from '@components/ButtonComposed'; import type {ListFilterContentWrapperProps} from '@components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent'; import ListFilterContent from '@components/Search/FilterComponents/ListFilterContent'; import useLocalize from '@hooks/useLocalize'; +import CONST from '@src/CONST'; + import React, {useState} from 'react'; function ListFilterContentPageWrapper({baseFilterKey, value: initialValue, isNegated: initialIsNegated, type, policyID, ready, onChange}: ListFilterContentWrapperProps) { @@ -24,11 +26,12 @@ function ListFilterContentPageWrapper({baseFilterKey, value: initialValue, isNeg onNegationChange={setIsNegatedValue} footer={ } /> ); From f39423367d200903182428f27f680d04b1742460 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 15 Jul 2026 11:56:05 +0800 Subject: [PATCH 24/25] lint --- src/components/Search/FilterDropdowns/TextFilterPopup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Search/FilterDropdowns/TextFilterPopup.tsx b/src/components/Search/FilterDropdowns/TextFilterPopup.tsx index ab865dbbec13..92c5d679c3f3 100644 --- a/src/components/Search/FilterDropdowns/TextFilterPopup.tsx +++ b/src/components/Search/FilterDropdowns/TextFilterPopup.tsx @@ -1,3 +1,4 @@ +import NegatableFilter from '@components/Search/FilterComponents/NegatableFilter'; import useTextFilterValidation from '@components/Search/hooks/useTextFilterValidation'; import type {ReportFieldTextKey, SearchTextFilterKeys} from '@components/Search/types'; import TextInput from '@components/TextInput'; @@ -14,7 +15,6 @@ import React, {useState} from 'react'; import type {PopoverComponentProps} from './FilterPopupButton'; -import NegatableFilter from '../FilterComponents/NegatableFilter'; import BasePopup from './BasePopup'; type TextFilterPopupProps = { From fac08fd3c7c016efcb36357526bc16f41ca010b4 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 15 Jul 2026 12:00:01 +0800 Subject: [PATCH 25/25] put the not prefix in a const --- src/CONST/index.ts | 1 + src/libs/SearchQueryUtils.ts | 8 ++++---- src/libs/SearchUIUtils.ts | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 7d37ca62ac30..2898ddc0a143 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7338,6 +7338,7 @@ const CONST = { [this.TABLE_COLUMNS.GROUP_WITHDRAWAL_STATUS]: 'group-withdrawal-status', }; }, + NOT_PREFIX: '-', NOT_MODIFIER: 'Not', DATE_MODIFIERS: { ON: 'On', diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 94e2490766dc..207a0e45f14e 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -358,7 +358,7 @@ function buildFilterValuesString(filterName: string, queryFilters: QueryFilter[] } else if (index !== 0 && (previousValueHasSameOp || nextValueHasSameOp)) { filterValueString += `${delimiter}${sanitizeSearchValue(queryFilter.value.toString())}`; } else if (queryFilter.operator === CONST.SEARCH.SYNTAX_OPERATORS.NOT_EQUAL_TO) { - filterValueString += ` -${filterName}:${sanitizeSearchValue(queryFilter.value.toString())}`; + filterValueString += ` ${CONST.SEARCH.NOT_PREFIX}${filterName}:${sanitizeSearchValue(queryFilter.value.toString())}`; } else if (queryFilter.operator === CONST.SEARCH.SYNTAX_OPERATORS.RANGE) { const rangeBoundaries = parseRangeQueryValue(queryFilter.value.toString()); if (rangeBoundaries.from) { @@ -547,13 +547,13 @@ function getQueryHashes(query: SearchQueryJSON) { orderedQuery += `${CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE}:${query.type}`; const status = getFilterFromQuery(query, CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS); - orderedQuery += ` ${status.isNegated ? '-' : ''}${CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS}:${status.value?.join(',') ?? ''}`; + orderedQuery += ` ${status.isNegated ? CONST.SEARCH.NOT_PREFIX : ''}${CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS}:${status.value?.join(',') ?? ''}`; orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY}:${query.groupBy}`; const policyID = getFilterFromQuery(query, CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID); if (policyID.value) { - orderedQuery += ` ${policyID.isNegated ? '-' : ''}${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${policyID.value.join(',')} `; + orderedQuery += ` ${policyID.isNegated ? CONST.SEARCH.NOT_PREFIX : ''}${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${policyID.value.join(',')} `; } const filterSet = new Set(orderedQuery); @@ -893,7 +893,7 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial