Repository navigation
[Payment due @truph01] fix: Add Contains / Equal to match type for the Merchant filter [80823] #83733
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
neil-marcellini
merged 92 commits into
Expensify:main
from
HiepPP:fix/80823-merchant-contains-operator
Oct 14, 2026
+2,186
−715
Merged
[Payment due @truph01] fix: Add Contains / Equal to match type for the Merchant filter [80823] #83733
Changes from all commits
Commits
Show all changes
92 commits
Select commit
Hold shift + click to select a range
21cac2d
fix: Use contains operator for merchant filter in advanced search
HiepPP 167132a
fix: Use contains operator for merchant filter in advanced search
HiepPP 5263040
Merge branch 'main' into fix/80823-merchant-contains-operator
HiepPP e869214
Merge branch 'main' into fix/80823-merchant-contains-operator
HiepPP 390eed1
Regenerate parser files with Peggy 4.0.3 to match package.json
HiepPP fda8c05
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP 62cf077
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP 9d6ff9c
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP fb5f7ad
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP 15482e4
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP 4219ec5
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP 556ffab
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP a7097b8
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP c033992
chore(search): fix parser CI checks
HiepPP b28ded0
Merge branch 'main' of github.com:Expensify/App into fix/80823-mercha…
HiepPP db675eb
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP c74520d
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP 6922311
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP 234ff70
feat(search): add merchant match type selector
HiepPP f5b6713
chore(i18n): generate locale translations for merchant match type keys
HiepPP c532a99
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP 5e91743
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP b2042f8
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP 24cc2e1
fix: use SingleSelectListItem for merchant match type selector
HiepPP 3e69172
fix: inline merchant filter input under match type option
HiepPP d7ae22f
fix: match Amount filter spacing for merchant input (mv2)
HiepPP f4dca8e
refactor: align merchant filter input with Amount via containerStyles
HiepPP 917b615
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP c8393b9
style: apply oxfmt to merge-resolved search filter files
HiepPP 2a54c3b
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP b6d73ba
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP 30b6796
fix(search): escape contains operator in keyword queries
HiepPP 5dcf473
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP 95d9396
fix(search): reject negated contains filters
HiepPP 1387f0c
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP e1d2f61
Fix merchant operator rows overlapping the value input on native
HiepPP d6d4084
Merge remote-tracking branch 'fork/fix/80823-merchant-contains-operat…
HiepPP 8f6b401
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP d3bf79e
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP d1fa1cd
Add missing merchant filter translation to Greek (el)
HiepPP 2a353ed
fix(search): hide merchant match types when negated
HiepPP 0164d54
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP 81247ea
fix(search): align merchant filter with rules layout
HiepPP b9cb8dc
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP d664814
fix(search): address review feedback on merchant contains operator
HiepPP dd4a4fc
fix(search): show merchant match type on filter chip and chip popup
HiepPP e1cf8c7
fix(search): preserve legacy merchant contains semantics
HiepPP 7164be0
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP bfe6153
fix(search): consolidate merchant popup imports
HiepPP e3b753b
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP e98d311
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP eb3f0cb
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP b214d46
refactor(search): use search-scoped merchant translations
HiepPP a76d328
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP a677f33
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP bc389b2
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP f56eaac
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP 5895f50
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP 2d7e2e8
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP 2d7013b
Merge origin/main into fix/80823-merchant-contains-operator
HiepPP 6c98d84
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP e636a74
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP c3130bc
Merge origin/main into fix/80823-merchant-contains-operator
HiepPP f30c813
fix(search): default merchant filters to contains
HiepPP 98667e4
Merge remote-tracking branch 'myfork/fix/80823-merchant-contains-oper…
HiepPP 15d41eb
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP 83bd615
fix(search): preserve merchant lists and isolate filter UI
HiepPP 268f398
chore: merge Expensify main into merchant filter branch
HiepPP 7fb75a9
refactor(search): isolate merchant filter chip popup
HiepPP 631a35d
refactor(search): remove redundant text filter wrapper
HiepPP 23b9ecc
fix(search): preserve mixed merchant predicates in filter updates
HiepPP 4b7c234
fix(search): preserve merchant lists through filter updates
HiepPP 7b33baf
fix(search): retain merchant lists when changing match type
HiepPP d16816d
chore: merge main into merchant filter branch
HiepPP 0154b5e
fix(search): apply merchant conditions to all clauses
HiepPP 2f84247
chore(search): merge main into merchant filter branch
HiepPP 31630f8
fix(search): address merchant filter review feedback
HiepPP 0e01cb9
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP b3272ae
test(search): expect contains for merchant filter in compare parser test
HiepPP 4fbb097
Merge fix/80823-merchant-contains-operator into latest main
HiepPP 45ff0ef
Drop the unrelated ReceiptTypeValues export
HiepPP cfc6130
Remove the dead exactMatchFilterKeys code path
HiepPP 380f75d
Extract buildMerchantFilterQuery from the filter query builder
HiepPP afdf370
Reuse TextInputFilterContent for the Merchant filter
HiepPP 93596d7
Load the English locale in SearchSavePageTest
HiepPP 58f320b
Render SearchColumnsPage inside OnyxListItemProvider in its test
HiepPP 8b1ff74
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP b179aab
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP 71e3304
Merge commit '97f79f1c31713fc55f27d0e522da391c0cfa465f' into fix/8082…
HiepPP 33151be
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP 125ca02
Update main's Spend footer tests for the merchant contains operator
HiepPP 37fd19d
Merge remote-tracking branch 'origin/main' into fix/80823-merchant-co…
HiepPP File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
108 changes: 108 additions & 0 deletions
108
src/components/Search/FilterComponents/AdvancedFilters/MerchantFilterContent.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| /** | ||
| * Renders the advanced Merchant filter input, match type, and negation controls. | ||
| */ | ||
| import MerchantMatchTypeSelector from '@components/Search/FilterComponents/MerchantMatchTypeSelector'; | ||
|
|
||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
|
|
||
| import CONST from '@src/CONST'; | ||
| import FILTER_KEYS from '@src/types/form/SearchAdvancedFiltersForm'; | ||
| import type {MerchantMatchType, SearchAdvancedFiltersForm} from '@src/types/form/SearchAdvancedFiltersForm'; | ||
|
|
||
| import type {StyleProp, ViewStyle} from 'react-native'; | ||
| import type {ValueOf} from 'type-fest'; | ||
|
|
||
| import React, {useState} from 'react'; | ||
| import {View} from 'react-native'; | ||
|
|
||
| import TextInputFilterContent from './TextInputFilterContent'; | ||
|
|
||
| type MerchantFilterContentProps = { | ||
| /** The Merchant filter key. */ | ||
| baseFilterKey: typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT; | ||
|
|
||
| /** The current Merchant value. */ | ||
| value: string | undefined; | ||
|
|
||
| /** Whether the filter currently uses the negated value. */ | ||
| isNegated: boolean; | ||
|
|
||
| /** The current Merchant match type. */ | ||
| merchantOperator?: MerchantMatchType; | ||
|
|
||
| /** The button size used for confirmation. */ | ||
| buttonSize?: Exclude<ValueOf<typeof CONST.BUTTON_SIZE>, typeof CONST.BUTTON_SIZE.SMALL>; | ||
|
|
||
| /** The label used when the filter applies immediately. */ | ||
| buttonText?: string; | ||
|
|
||
| /** The Sentry label for the confirmation button. */ | ||
| sentryLabel?: string; | ||
|
|
||
| /** Whether the input should receive focus automatically. */ | ||
| autoFocus?: boolean; | ||
|
|
||
| /** Additional styles for the filter content. */ | ||
| style?: StyleProp<ViewStyle>; | ||
|
|
||
| /** Additional styles for the confirmation button. */ | ||
| buttonStyles?: StyleProp<ViewStyle>; | ||
|
|
||
| /** Called with the updated Merchant filter form values. */ | ||
| onChange: (values: Partial<SearchAdvancedFiltersForm>) => void; | ||
| }; | ||
|
|
||
| function MerchantFilterContent({ | ||
| baseFilterKey, | ||
| value, | ||
| isNegated, | ||
| merchantOperator: initialMerchantOperator, | ||
| buttonSize, | ||
| buttonText, | ||
| sentryLabel, | ||
| autoFocus, | ||
| style, | ||
| buttonStyles, | ||
| onChange, | ||
| }: MerchantFilterContentProps) { | ||
| const styles = useThemeStyles(); | ||
| const [merchantOperator, setMerchantOperator] = useState<MerchantMatchType>(initialMerchantOperator ?? CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS); | ||
|
|
||
| const updateMerchantFilter = (newValue: string | undefined, newIsNegated: boolean) => { | ||
| onChange({ | ||
| [FILTER_KEYS.MERCHANT]: newIsNegated ? undefined : newValue, | ||
| [FILTER_KEYS.MERCHANT_NOT]: newIsNegated ? newValue : undefined, | ||
| [FILTER_KEYS.MERCHANT_OPERATOR]: newIsNegated ? undefined : merchantOperator, | ||
| }); | ||
| }; | ||
|
|
||
| // A negated Merchant filter always uses the not-equal operator, so the match type is hidden. | ||
| const renderMatchTypeSelector = (isFilterNegated: boolean) => | ||
| isFilterNegated ? null : ( | ||
| <View style={[styles.flex1, styles.mt2]}> | ||
| <MerchantMatchTypeSelector | ||
| value={merchantOperator} | ||
| onChange={setMerchantOperator} | ||
| /> | ||
| </View> | ||
| ); | ||
|
|
||
| return ( | ||
| <TextInputFilterContent | ||
| baseFilterKey={baseFilterKey} | ||
| value={value} | ||
| isNegated={isNegated} | ||
| size={buttonSize} | ||
| autoFocus={autoFocus} | ||
| style={style} | ||
| buttonText={buttonText} | ||
| buttonStyles={buttonStyles} | ||
| sentryLabel={sentryLabel} | ||
| renderBelowInput={renderMatchTypeSelector} | ||
| onChange={updateMerchantFilter} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| export default MerchantFilterContent; | ||
| export type {MerchantFilterContentProps}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 62 additions & 0 deletions
62
src/components/Search/FilterComponents/MerchantMatchTypeSelector.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| /** | ||
| * Shared Merchant match type selector for search filter surfaces. | ||
| */ | ||
| import SelectionList from '@components/SelectionList'; | ||
| import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; | ||
| import type {ListItem} from '@components/SelectionList/ListItem/types'; | ||
| import Text from '@components/Text'; | ||
|
|
||
| import useLocalize from '@hooks/useLocalize'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
|
|
||
| import variables from '@styles/variables'; | ||
|
|
||
| import CONST from '@src/CONST'; | ||
| import type {MerchantMatchType} from '@src/types/form/SearchAdvancedFiltersForm'; | ||
|
|
||
| import React from 'react'; | ||
| import {View} from 'react-native'; | ||
|
|
||
| type MerchantMatchTypeSelectorProps = { | ||
| /** Currently selected Merchant match type. */ | ||
| value: MerchantMatchType; | ||
|
|
||
| /** Updates the selected Merchant match type. */ | ||
| onChange: (value: MerchantMatchType) => void; | ||
| }; | ||
|
|
||
| function MerchantMatchTypeSelector({value, onChange}: MerchantMatchTypeSelectorProps) { | ||
| const {translate} = useLocalize(); | ||
| const styles = useThemeStyles(); | ||
| const items: Array<ListItem<MerchantMatchType>> = [ | ||
| { | ||
| keyForList: CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS, | ||
| text: translate('search.filters.merchant.contains'), | ||
| isSelected: value === CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS, | ||
| }, | ||
| { | ||
| keyForList: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, | ||
| text: translate('search.filters.merchant.equalTo'), | ||
| isSelected: value === CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, | ||
| }, | ||
| ]; | ||
|
|
||
| return ( | ||
| <> | ||
| <View style={[styles.pb2, styles.ph5]}> | ||
| <Text style={[styles.textLabelSupporting]}>{translate('search.filters.merchant.matchType')}</Text> | ||
| </View> | ||
| {/* Inside a ScrollView a flex list can shrink to zero height when the keyboard is open, so reserve room for every row */} | ||
| <View style={[styles.flex1, {minHeight: items.length * variables.optionRowHeight}]}> | ||
| <SelectionList | ||
| data={items} | ||
| ListItem={SingleSelectListItem} | ||
| onSelectRow={(item) => onChange(item.keyForList)} | ||
| shouldSingleExecuteRowSelect | ||
| /> | ||
| </View> | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| export default MerchantMatchTypeSelector; |
68 changes: 68 additions & 0 deletions
68
src/components/Search/FilterDropdowns/MerchantFilterPopup.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| /** | ||
| * Edits the Merchant filter value and match type from its search filter chip. | ||
| */ | ||
| import MerchantFilterContent from '@components/Search/FilterComponents/AdvancedFilters/MerchantFilterContent'; | ||
|
|
||
| import useLocalize from '@hooks/useLocalize'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
|
|
||
| import {getFilterNegatableValue} from '@libs/SearchUIUtils'; | ||
|
|
||
| import type CONST from '@src/CONST'; | ||
| import FILTER_KEYS from '@src/types/form/SearchAdvancedFiltersForm'; | ||
| import type {SearchAdvancedFiltersForm} from '@src/types/form/SearchAdvancedFiltersForm'; | ||
|
|
||
| import React from 'react'; | ||
|
|
||
| import type {PopoverComponentProps} from './FilterPopupButton'; | ||
|
|
||
| import BasePopup from './BasePopup'; | ||
|
|
||
| type MerchantFilterPopupProps = { | ||
| /** The Merchant filter key. */ | ||
| baseFilterKey: typeof CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT; | ||
| /** The current search filter form values. */ | ||
| values: Partial<SearchAdvancedFiltersForm> | undefined; | ||
| /** The translated filter label. */ | ||
| label: string; | ||
| /** Closes the filter popup. */ | ||
| closeOverlay: PopoverComponentProps['closeOverlay']; | ||
| /** Applies the updated Merchant filter values. */ | ||
| updateFilterForm: (value: Partial<SearchAdvancedFiltersForm>) => void; | ||
| }; | ||
|
|
||
| function MerchantFilterPopup({baseFilterKey, values, label, updateFilterForm, closeOverlay}: MerchantFilterPopupProps) { | ||
| const {translate} = useLocalize(); | ||
| const styles = useThemeStyles(); | ||
| const {isNegated, value} = getFilterNegatableValue(baseFilterKey, values); | ||
| const applySentryLabel = `Search-FilterPopupApply-${baseFilterKey}`; | ||
|
|
||
| const applyChanges = (newValues: Partial<SearchAdvancedFiltersForm>) => { | ||
| updateFilterForm(newValues); | ||
| closeOverlay(); | ||
| }; | ||
|
|
||
| // The content renders its own padded Apply button, so the popup only provides the label and the spacing ActionButtons would add. | ||
| return ( | ||
| <BasePopup | ||
| label={label} | ||
| shouldShowActionButtons={false} | ||
| style={styles.pb0} | ||
| onApply={closeOverlay} | ||
| applySentryLabel={applySentryLabel} | ||
| > | ||
| <MerchantFilterContent | ||
| baseFilterKey={baseFilterKey} | ||
| value={value} | ||
| isNegated={isNegated} | ||
| merchantOperator={values?.[FILTER_KEYS.MERCHANT_OPERATOR]} | ||
| buttonText={translate('common.apply')} | ||
| sentryLabel={applySentryLabel} | ||
| buttonStyles={styles.mt2} | ||
| onChange={applyChanges} | ||
| /> | ||
| </BasePopup> | ||
| ); | ||
| } | ||
|
|
||
| export default MerchantFilterPopup; |
24 changes: 24 additions & 0 deletions
24
...s/Search/FilterDropdowns/SearchAdvancedFiltersPopup/MerchantFilterContentPopupWrapper.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import MerchantFilterContent from '@components/Search/FilterComponents/AdvancedFilters/MerchantFilterContent'; | ||
| import type {MerchantFilterContentWrapperProps} from '@components/Search/FilterComponents/AdvancedFilters/SearchAdvancedFiltersContent'; | ||
|
|
||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
|
|
||
| import React from 'react'; | ||
|
|
||
| function MerchantFilterContentPopupWrapper({baseFilterKey, value, isNegated, merchantOperator, buttonText, onChange}: MerchantFilterContentWrapperProps) { | ||
| const styles = useThemeStyles(); | ||
|
|
||
| return ( | ||
| <MerchantFilterContent | ||
| baseFilterKey={baseFilterKey} | ||
| value={value} | ||
| isNegated={isNegated} | ||
| merchantOperator={merchantOperator} | ||
| buttonText={buttonText} | ||
| style={[styles.pt6]} | ||
| onChange={onChange} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| export default MerchantFilterContentPopupWrapper; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is
TextInputFilterContentplus the match-type selector, but without itsScrollViewandshouldButtonBeInScrollViewhandling.Should we add an optional slot to
TextInputFilterContentinstead of forking it?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in c2a4c59, added a generic
renderBelowInputslot toTextInputFilterContent.