Skip to content

Commit 91ed111

Browse files
committed
refactor BankAccountSelector to sectioned picker mirroring CardSelector
1 parent c8fb6fb commit 91ed111

1 file changed

Lines changed: 133 additions & 25 deletions

File tree

src/components/Search/FilterComponents/BankAccountSelector.tsx

Lines changed: 133 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,69 @@
11
import React from 'react';
22
import {View} from 'react-native';
3+
import ActivityIndicator from '@components/ActivityIndicator';
34
import Icon from '@components/Icon';
45
import getBankIcon from '@components/Icon/BankIcons';
56
import 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';
612
import useOnyx from '@hooks/useOnyx';
713
import useResponsiveLayout from '@hooks/useResponsiveLayout';
14+
import useTheme from '@hooks/useTheme';
815
import 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';
1018
import variables from '@styles/variables';
1119
import CONST from '@src/CONST';
1220
import ONYXKEYS from '@src/ONYXKEYS';
13-
import MultiSelect from './MultiSelect';
21+
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
22+
import ListFilterView from './ListFilterViewWrapper';
1423

1524
type 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

2642
function 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

Comments
 (0)