11import React from 'react' ;
22import { View } from 'react-native' ;
3+ import ActivityIndicator from '@components/ActivityIndicator' ;
34import Icon from '@components/Icon' ;
45import getBankIcon from '@components/Icon/BankIcons' ;
56import type { SearchFilterCommonProps } from '@components/Search/types' ;
7+ import MultiSelectListItem from '@components/SelectionList/ListItem/MultiSelectListItem' ;
8+ import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections' ;
9+ import type { TextInputOptions } from '@components/SelectionList/types' ;
10+ import useDebouncedState from '@hooks/useDebouncedState' ;
11+ import useLocalize from '@hooks/useLocalize' ;
612import useOnyx from '@hooks/useOnyx' ;
713import useResponsiveLayout from '@hooks/useResponsiveLayout' ;
14+ import useTheme from '@hooks/useTheme' ;
815import useThemeStyles from '@hooks/useThemeStyles' ;
9- import { getBankAccountSearchLabel } from '@libs/BankAccountUtils' ;
16+ import { getBankAccountSearchLabel , isBankAccountPartiallySetup } from '@libs/BankAccountUtils' ;
17+ import type { SkeletonSpanReasonAttributes } from '@libs/telemetry/useSkeletonSpan' ;
1018import variables from '@styles/variables' ;
1119import CONST from '@src/CONST' ;
1220import ONYXKEYS from '@src/ONYXKEYS' ;
13- import MultiSelect from './MultiSelect' ;
21+ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue' ;
22+ import ListFilterView from './ListFilterViewWrapper' ;
1423
1524type BankAccountSelectorProps = SearchFilterCommonProps < string [ ] | undefined > ;
1625
17- type BankAccountItem = {
26+ /** Row item rendered by SelectionListWithSections for a single bank account. */
27+ type BankAccountFilterItem = {
1828 /** Display label, e.g. `Chase xx1234`. */
1929 text : string ;
20- /** Bank account id stringified, used as the filter value. */
30+ /** Bank account ID stringified — used as both the stable list key and the filter value. */
31+ keyForList : string ;
32+ /** Same as keyForList; kept on the item so onChange logic mirrors CardSelector. */
2133 value : string ;
22- /** Bank icon wrapper rendered to the left of the row. */
34+ /** Whether this account is currently selected in the filter. */
35+ isSelected : boolean ;
36+ /** Bank icon wrapper rendered at the start of the row. */
2337 leftElement : React . JSX . Element ;
38+ /** Last four digits of the account number — searched in addition to the display text. */
39+ lastFour : string ;
2440} ;
2541
2642function BankAccountSelector ( { value = [ ] , selectionListTextInputStyle, selectionListStyle, autoFocus, footer, onChange} : BankAccountSelectorProps ) {
43+ const theme = useTheme ( ) ;
2744 const styles = useThemeStyles ( ) ;
45+ const { translate} = useLocalize ( ) ;
2846 const { isLargeScreenWidth} = useResponsiveLayout ( ) ;
29- const [ bankAccountList ] = useOnyx ( ONYXKEYS . BANK_ACCOUNT_LIST ) ;
47+ const [ bankAccountList , bankAccountListMetadata ] = useOnyx ( ONYXKEYS . BANK_ACCOUNT_LIST ) ;
48+ const [ searchTerm , debouncedSearchTerm , setSearchTerm ] = useDebouncedState ( '' ) ;
3049
31- const bankAccountItems : BankAccountItem [ ] = [ ] ;
50+ const openItems : BankAccountFilterItem [ ] = [ ] ;
51+ const closedItems : BankAccountFilterItem [ ] = [ ] ;
3252 for ( const bankAccount of Object . values ( bankAccountList ?? { } ) ) {
3353 const bankAccountID = bankAccount ?. accountData ?. bankAccountID ;
3454 if ( ! bankAccountID ) {
3555 continue ;
3656 }
57+ if ( bankAccount ?. accountData ?. type !== CONST . BANK_ACCOUNT . TYPE . BUSINESS ) {
58+ continue ;
59+ }
60+ if ( isBankAccountPartiallySetup ( bankAccount ?. accountData ?. state ) ) {
61+ continue ;
62+ }
63+
64+ const key = bankAccountID . toString ( ) ;
3765 const bankName = bankAccount ?. accountData ?. additionalData ?. bankName ;
38- const label = getBankAccountSearchLabel ( bankAccount ) ;
66+ const accountNumber = bankAccount ?. accountData ?. accountNumber ?? '' ;
3967 const { icon, iconSize, iconStyles} = getBankIcon ( { bankName, styles, maxIconSize : isLargeScreenWidth ? variables . w28 : undefined } ) ;
4068 const leftElement = (
4169 < View style = { [ styles . mr3 , styles . alignItemsCenter , styles . justifyContentCenter ] } >
@@ -48,28 +76,108 @@ function BankAccountSelector({value = [], selectionListTextInputStyle, selection
4876 </ View >
4977 ) ;
5078
51- bankAccountItems . push ( {
52- text : label ,
53- value : bankAccountID . toString ( ) ,
79+ const item : BankAccountFilterItem = {
80+ text : getBankAccountSearchLabel ( bankAccount ) ,
81+ keyForList : key ,
82+ value : key ,
83+ isSelected : value . includes ( key ) ,
5484 leftElement,
55- } ) ;
85+ lastFour : accountNumber . slice ( - 4 ) ,
86+ } ;
87+
88+ if ( bankAccount ?. accountData ?. state === CONST . BANK_ACCOUNT . STATE . OPEN ) {
89+ openItems . push ( item ) ;
90+ } else {
91+ closedItems . push ( item ) ;
92+ }
5693 }
5794
58- const selectedBankAccounts = bankAccountItems . filter ( ( item ) => value . includes ( item . value ) ) ;
59- // Pin selected items to the top so the picker matches the Card filter behavior.
60- const sortedBankAccountItems = [ ...selectedBankAccounts , ...bankAccountItems . filter ( ( item ) => ! value . includes ( item . value ) ) ] ;
95+ const shouldShowSearchInput = openItems . length + closedItems . length >= CONST . STANDARD_LIST_ITEM_LIMIT ;
96+
97+ const searchFunction = ( item : BankAccountFilterItem ) =>
98+ item . text . toLocaleLowerCase ( ) . includes ( debouncedSearchTerm . toLocaleLowerCase ( ) ) || item . lastFour . toLocaleLowerCase ( ) . includes ( debouncedSearchTerm . toLocaleLowerCase ( ) ) ;
99+
100+ const selectedData = [ ...openItems , ...closedItems ] . filter ( ( item ) => item . isSelected && searchFunction ( item ) ) ;
101+ const unselectedOpenData = openItems . filter ( ( item ) => ! item . isSelected && searchFunction ( item ) ) ;
102+ const unselectedClosedData = closedItems . filter ( ( item ) => ! item . isSelected && searchFunction ( item ) ) ;
103+
104+ const itemCount = selectedData . length + unselectedOpenData . length + unselectedClosedData . length ;
105+ const sectionHeaderCount = unselectedClosedData . length > 0 ? 1 : 0 ;
106+
107+ const sections = [
108+ {
109+ title : undefined ,
110+ data : selectedData ,
111+ sectionIndex : 0 ,
112+ } ,
113+ {
114+ title : undefined ,
115+ data : unselectedOpenData ,
116+ sectionIndex : 1 ,
117+ } ,
118+ {
119+ title : translate ( 'search.filters.bankAccount.closedBankAccounts' ) ,
120+ data : unselectedClosedData ,
121+ sectionIndex : 2 ,
122+ } ,
123+ ] ;
124+
125+ const updateSelection = ( item : BankAccountFilterItem ) => {
126+ if ( ! item . keyForList ) {
127+ return ;
128+ }
129+ if ( item . isSelected ) {
130+ onChange ( value . filter ( ( id ) => id !== item . keyForList ) ) ;
131+ return ;
132+ }
133+ onChange ( [ ...value , item . keyForList ] ) ;
134+ } ;
135+
136+ const textInputOptions : TextInputOptions = {
137+ value : searchTerm ,
138+ label : translate ( 'common.search' ) ,
139+ onChangeText : setSearchTerm ,
140+ headerMessage : debouncedSearchTerm . trim ( ) && sections . every ( ( section ) => ! section . data . length ) ? translate ( 'common.noResultsFound' ) : '' ,
141+ style : {
142+ containerStyle : selectionListTextInputStyle ,
143+ } ,
144+ disableAutoFocus : ! autoFocus ,
145+ } ;
146+
147+ const isLoadingOnyxData = isLoadingOnyxValue ( bankAccountListMetadata ) ;
148+ const reasonAttributes : SkeletonSpanReasonAttributes = { context : 'SearchFiltersBankAccountPage' , isLoadingFromOnyx : isLoadingOnyxData } ;
61149
62150 return (
63- < MultiSelect
64- value = { selectedBankAccounts }
65- items = { sortedBankAccountItems }
66- isSearchable = { sortedBankAccountItems . length >= CONST . STANDARD_LIST_ITEM_LIMIT }
67- selectionListTextInputStyle = { selectionListTextInputStyle }
68- selectionListStyle = { selectionListStyle }
69- autoFocus = { autoFocus }
70- footer = { footer }
71- onChange = { ( bankAccounts ) => onChange ( bankAccounts . map ( ( item ) => item . value ) ) }
72- />
151+ < ListFilterView
152+ itemCount = { itemCount }
153+ itemHeight = { variables . optionRowHeight }
154+ isSearchable = { shouldShowSearchInput }
155+ extraHeight = { 28 * sectionHeaderCount }
156+ >
157+ { isLoadingOnyxData ? (
158+ < View style = { [ styles . flex1 , styles . flexColumn , styles . justifyContentCenter , styles . alignItemsCenter ] } >
159+ < ActivityIndicator
160+ color = { theme . spinner }
161+ size = { CONST . ACTIVITY_INDICATOR_SIZE . LARGE }
162+ style = { [ styles . pl3 ] }
163+ reasonAttributes = { reasonAttributes }
164+ />
165+ </ View >
166+ ) : (
167+ < SelectionListWithSections < BankAccountFilterItem >
168+ sections = { sections }
169+ ListItem = { MultiSelectListItem }
170+ onSelectRow = { updateSelection }
171+ shouldPreventDefaultFocusOnSelectRow = { false }
172+ shouldShowTextInput = { shouldShowSearchInput }
173+ textInputOptions = { textInputOptions }
174+ shouldStopPropagation
175+ canSelectMultiple
176+ style = { selectionListStyle }
177+ footerContent = { footer }
178+ />
179+ ) }
180+ </ ListFilterView >
73181 ) ;
74182}
75183
0 commit comments