Skip to content

Commit cd03f19

Browse files
authored
Merge pull request #46937 from nkdengineer/fix/45896
Fix: different behavior when using app and device back button in selection mode
2 parents 6d003de + 4a708bf commit cd03f19

File tree

9 files changed

+100
-40
lines changed

9 files changed

+100
-40
lines changed

src/components/Search/SearchContext.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import React, {useCallback, useContext, useMemo, useState} from 'react';
22
import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
33
import {isMoneyRequestReport} from '@libs/ReportUtils';
4-
import * as SearchUIUtils from '@libs/SearchUIUtils';
4+
import {isReportListItemType} from '@libs/SearchUIUtils';
55
import CONST from '@src/CONST';
66
import type ChildrenProps from '@src/types/utils/ChildrenProps';
77
import type {SearchContext, SelectedTransactions} from './types';
88

99
const defaultSearchContext = {
1010
currentSearchHash: -1,
11+
shouldTurnOffSelectionMode: false,
1112
selectedTransactions: {},
1213
selectedReports: [],
1314
setCurrentSearchHash: () => {},
@@ -25,17 +26,18 @@ function getReportsFromSelectedTransactions(data: TransactionListItemType[] | Re
2526
return (data ?? [])
2627
.filter(
2728
(item): item is ReportListItemType =>
28-
SearchUIUtils.isReportListItemType(item) &&
29+
isReportListItemType(item) &&
2930
isMoneyRequestReport(item) &&
3031
item?.transactions?.every((transaction: {keyForList: string | number}) => selectedTransactions[transaction.keyForList]?.isSelected),
3132
)
32-
.map((item) => ({reportID: item.reportID, action: item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW, total: item.total ?? 0, policyID: item.policyID ?? ''}));
33+
.map((item) => ({reportID: item.reportID, action: item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW, total: item.total ?? CONST.DEFAULT_NUMBER_ID, policyID: item.policyID}));
3334
}
3435

3536
function SearchContextProvider({children}: ChildrenProps) {
36-
const [searchContextData, setSearchContextData] = useState<Pick<SearchContext, 'currentSearchHash' | 'selectedTransactions' | 'selectedReports'>>({
37+
const [searchContextData, setSearchContextData] = useState<Pick<SearchContext, 'currentSearchHash' | 'selectedTransactions' | 'shouldTurnOffSelectionMode' | 'selectedReports'>>({
3738
currentSearchHash: defaultSearchContext.currentSearchHash,
3839
selectedTransactions: defaultSearchContext.selectedTransactions,
40+
shouldTurnOffSelectionMode: false,
3941
selectedReports: defaultSearchContext.selectedReports,
4042
});
4143

@@ -53,17 +55,19 @@ function SearchContextProvider({children}: ChildrenProps) {
5355
setSearchContextData((prevState) => ({
5456
...prevState,
5557
selectedTransactions,
58+
shouldTurnOffSelectionMode: false,
5659
selectedReports,
5760
}));
5861
}, []);
5962

6063
const clearSelectedTransactions = useCallback(
61-
(searchHash?: number) => {
64+
(searchHash?: number, shouldTurnOffSelectionMode = false) => {
6265
if (searchHash === searchContextData.currentSearchHash) {
6366
return;
6467
}
6568
setSearchContextData((prevState) => ({
6669
...prevState,
70+
shouldTurnOffSelectionMode,
6771
selectedTransactions: {},
6872
selectedReports: [],
6973
}));

src/components/Search/SearchPageHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) {
183183

184184
const paymentData = (
185185
selectedReports.length
186-
? selectedReports.map((report) => ({reportID: report.reportID, amount: report.total, paymentType: lastPaymentMethods[report.policyID]}))
186+
? selectedReports.map((report) => ({reportID: report.reportID, amount: report.total, paymentType: lastPaymentMethods[`${report.policyID}`]}))
187187
: Object.values(selectedTransactions).map((transaction) => ({
188188
reportID: transaction.reportID,
189189
amount: transaction.amount,

src/components/Search/index.tsx

Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,25 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
1515
import useSearchHighlightAndScroll from '@hooks/useSearchHighlightAndScroll';
1616
import useThemeStyles from '@hooks/useThemeStyles';
1717
import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
18-
import * as SearchActions from '@libs/actions/Search';
19-
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
18+
import {createTransactionThread, search} from '@libs/actions/Search';
19+
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
2020
import Log from '@libs/Log';
2121
import memoize from '@libs/memoize';
2222
import isSearchTopmostCentralPane from '@libs/Navigation/isSearchTopmostCentralPane';
2323
import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types';
24-
import * as ReportUtils from '@libs/ReportUtils';
25-
import * as SearchQueryUtils from '@libs/SearchQueryUtils';
26-
import * as SearchUIUtils from '@libs/SearchUIUtils';
27-
import * as TransactionUtils from '@libs/TransactionUtils';
24+
import {generateReportID} from '@libs/ReportUtils';
25+
import {buildSearchQueryString} from '@libs/SearchQueryUtils';
26+
import {
27+
getListItem,
28+
getSections,
29+
getSortedSections,
30+
isReportActionListItemType,
31+
isReportListItemType,
32+
isSearchResultsEmpty as isSearchResultsEmptyUtil,
33+
isTransactionListItemType,
34+
shouldShowYear as shouldShowYearUtil,
35+
} from '@libs/SearchUIUtils';
36+
import {isOnHold} from '@libs/TransactionUtils';
2837
import Navigation from '@navigation/Navigation';
2938
import type {AuthScreensParamList} from '@navigation/types';
3039
import EmptySearchView from '@pages/Search/EmptySearchView';
@@ -57,7 +66,7 @@ function mapTransactionItemToSelectedEntry(item: TransactionListItemType): [stri
5766
isSelected: true,
5867
canDelete: item.canDelete,
5968
canHold: item.canHold,
60-
isHeld: TransactionUtils.isOnHold(item),
69+
isHeld: isOnHold(item),
6170
canUnhold: item.canUnhold,
6271
action: item.action,
6372
reportID: item.reportID,
@@ -77,14 +86,14 @@ function mapToItemWithSelectionInfo(
7786
canSelectMultiple: boolean,
7887
shouldAnimateInHighlight: boolean,
7988
) {
80-
if (SearchUIUtils.isReportActionListItemType(item)) {
89+
if (isReportActionListItemType(item)) {
8190
return {
8291
...item,
8392
shouldAnimateInHighlight,
8493
};
8594
}
8695

87-
return SearchUIUtils.isTransactionListItemType(item)
96+
return isTransactionListItemType(item)
8897
? mapToTransactionItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight)
8998
: {
9099
...item,
@@ -107,7 +116,7 @@ function prepareTransactionsList(item: TransactionListItemType, selectedTransact
107116
isSelected: true,
108117
canDelete: item.canDelete,
109118
canHold: item.canHold,
110-
isHeld: TransactionUtils.isOnHold(item),
119+
isHeld: isOnHold(item),
111120
canUnhold: item.canUnhold,
112121
action: item.action,
113122
reportID: item.reportID,
@@ -127,8 +136,16 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
127136
const navigation = useNavigation<PlatformStackNavigationProp<AuthScreensParamList>>();
128137
const isFocused = useIsFocused();
129138
const [lastNonEmptySearchResults, setLastNonEmptySearchResults] = useState<SearchResults | undefined>(undefined);
130-
const {setCurrentSearchHash, setSelectedTransactions, selectedTransactions, clearSelectedTransactions, setShouldShowStatusBarLoading, lastSearchType, setLastSearchType} =
131-
useSearchContext();
139+
const {
140+
setCurrentSearchHash,
141+
setSelectedTransactions,
142+
selectedTransactions,
143+
clearSelectedTransactions,
144+
shouldTurnOffSelectionMode,
145+
setShouldShowStatusBarLoading,
146+
lastSearchType,
147+
setLastSearchType,
148+
} = useSearchContext();
132149
const {selectionMode} = useMobileSelectionMode(false);
133150
const [offset, setOffset] = useState(0);
134151

@@ -158,6 +175,13 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
158175
setCurrentSearchHash(hash);
159176
}, [hash, clearSelectedTransactions, setCurrentSearchHash]);
160177

178+
useEffect(() => {
179+
const selectedKeys = Object.keys(selectedTransactions).filter((key) => selectedTransactions[key]);
180+
if (selectedKeys.length === 0 && selectionMode?.isEnabled && shouldTurnOffSelectionMode) {
181+
turnOffMobileSelectionMode();
182+
}
183+
}, [selectedTransactions, selectionMode?.isEnabled, shouldTurnOffSelectionMode]);
184+
161185
useEffect(() => {
162186
const selectedKeys = Object.keys(selectedTransactions).filter((key) => selectedTransactions[key]);
163187
if (!isSmallScreenWidth) {
@@ -176,12 +200,12 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
176200
return;
177201
}
178202

179-
SearchActions.search({queryJSON, offset});
203+
search({queryJSON, offset});
180204
}, [isOffline, offset, queryJSON]);
181205

182206
const getItemHeight = useCallback(
183207
(item: TransactionListItemType | ReportListItemType | ReportActionListItemType) => {
184-
if (SearchUIUtils.isTransactionListItemType(item) || SearchUIUtils.isReportActionListItemType(item)) {
208+
if (isTransactionListItemType(item) || isReportActionListItemType(item)) {
185209
return isLargeScreenWidth ? variables.optionRowHeight + listItemPadding : transactionItemMobileHeight + listItemPadding;
186210
}
187211

@@ -229,14 +253,14 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
229253

230254
const shouldShowLoadingState = !isOffline && !isDataLoaded;
231255
const shouldShowLoadingMoreItems = !shouldShowLoadingState && searchResults?.search?.isLoading && searchResults?.search?.offset > 0;
232-
const isSearchResultsEmpty = !searchResults?.data || SearchUIUtils.isSearchResultsEmpty(searchResults);
256+
const isSearchResultsEmpty = !searchResults?.data || isSearchResultsEmptyUtil(searchResults);
233257
const prevIsSearchResultEmpty = usePrevious(isSearchResultsEmpty);
234258

235259
const data = useMemo(() => {
236260
if (searchResults === undefined) {
237261
return [];
238262
}
239-
return SearchUIUtils.getSections(type, status, searchResults.data, searchResults.search);
263+
return getSections(type, status, searchResults.data, searchResults.search);
240264
}, [searchResults, status, type]);
241265

242266
useEffect(() => {
@@ -260,7 +284,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
260284
newTransactionList[transaction.transactionID] = {
261285
action: transaction.action,
262286
canHold: transaction.canHold,
263-
isHeld: TransactionUtils.isOnHold(transaction),
287+
isHeld: isOnHold(transaction),
264288
canUnhold: transaction.canUnhold,
265289
isSelected: selectedTransactions[transaction.transactionID].isSelected,
266290
canDelete: transaction.canDelete,
@@ -281,7 +305,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
281305
newTransactionList[transaction.transactionID] = {
282306
action: transaction.action,
283307
canHold: transaction.canHold,
284-
isHeld: TransactionUtils.isOnHold(transaction),
308+
isHeld: isOnHold(transaction),
285309
canUnhold: transaction.canUnhold,
286310
isSelected: selectedTransactions[transaction.transactionID].isSelected,
287311
canDelete: transaction.canDelete,
@@ -328,8 +352,8 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
328352
return <FullPageOfflineBlockingView>{null}</FullPageOfflineBlockingView>;
329353
}
330354

331-
const ListItem = SearchUIUtils.getListItem(type, status);
332-
const sortedData = SearchUIUtils.getSortedSections(type, status, data, sortBy, sortOrder);
355+
const ListItem = getListItem(type, status);
356+
const sortedData = getSortedSections(type, status, data, sortBy, sortOrder);
333357
const isChat = type === CONST.SEARCH.DATA_TYPES.CHAT;
334358
const sortedSelectedData = sortedData.map((item) => {
335359
const baseKey = isChat
@@ -364,10 +388,10 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
364388
}
365389

366390
const toggleTransaction = (item: TransactionListItemType | ReportListItemType | ReportActionListItemType) => {
367-
if (SearchUIUtils.isReportActionListItemType(item)) {
391+
if (isReportActionListItemType(item)) {
368392
return;
369393
}
370-
if (SearchUIUtils.isTransactionListItemType(item)) {
394+
if (isTransactionListItemType(item)) {
371395
if (!item.keyForList) {
372396
return;
373397
}
@@ -398,21 +422,21 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
398422

399423
const openReport = (item: TransactionListItemType | ReportListItemType | ReportActionListItemType) => {
400424
const isFromSelfDM = item.reportID === CONST.REPORT.UNREPORTED_REPORTID;
401-
let reportID = SearchUIUtils.isTransactionListItemType(item) && (!item.isFromOneTransactionReport || isFromSelfDM) ? item.transactionThreadReportID : item.reportID;
425+
let reportID = isTransactionListItemType(item) && (!item.isFromOneTransactionReport || isFromSelfDM) ? item.transactionThreadReportID : item.reportID;
402426

403427
if (!reportID) {
404428
return;
405429
}
406430

407431
// If we're trying to open a legacy transaction without a transaction thread, let's create the thread and navigate the user
408-
if (SearchUIUtils.isTransactionListItemType(item) && reportID === '0' && item.moneyRequestReportActionID) {
409-
reportID = ReportUtils.generateReportID();
410-
SearchActions.createTransactionThread(hash, item.transactionID, reportID, item.moneyRequestReportActionID);
432+
if (isTransactionListItemType(item) && reportID === '0' && item.moneyRequestReportActionID) {
433+
reportID = generateReportID();
434+
createTransactionThread(hash, item.transactionID, reportID, item.moneyRequestReportActionID);
411435
}
412436

413437
const backTo = Navigation.getActiveRoute();
414438

415-
if (SearchUIUtils.isReportActionListItemType(item)) {
439+
if (isReportActionListItemType(item)) {
416440
const reportActionID = item.reportActionID;
417441
Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID, reportActionID, backTo}));
418442
return;
@@ -448,11 +472,11 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
448472
};
449473

450474
const onSortPress = (column: SearchColumnType, order: SortOrder) => {
451-
const newQuery = SearchQueryUtils.buildSearchQueryString({...queryJSON, sortBy: column, sortOrder: order});
475+
const newQuery = buildSearchQueryString({...queryJSON, sortBy: column, sortOrder: order});
452476
navigation.setParams({q: newQuery});
453477
};
454478

455-
const shouldShowYear = SearchUIUtils.shouldShowYear(searchResults?.data);
479+
const shouldShowYear = shouldShowYearUtil(searchResults?.data);
456480
const shouldShowSorting = !Array.isArray(status) && sortableSearchStatuses.includes(status);
457481

458482
return (
@@ -477,7 +501,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
477501
)
478502
}
479503
isSelected={(item) =>
480-
status !== CONST.SEARCH.STATUS.EXPENSE.ALL && SearchUIUtils.isReportListItemType(item)
504+
status !== CONST.SEARCH.STATUS.EXPENSE.ALL && isReportListItemType(item)
481505
? item.transactions.some((transaction) => selectedTransactions[transaction.keyForList]?.isSelected)
482506
: !!item.isSelected
483507
}
@@ -501,7 +525,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
501525
onSelectRow={openReport}
502526
getItemHeight={getItemHeightMemoized}
503527
shouldSingleExecuteRowSelect
504-
shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
528+
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
505529
shouldPreventDefault={false}
506530
listHeaderWrapperStyle={[styles.ph8, styles.pt3]}
507531
containerStyle={[styles.pv0, type === CONST.SEARCH.DATA_TYPES.CHAT && !isSmallScreenWidth && styles.pt3]}

src/components/Search/types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ type SelectedTransactions = Record<string, SelectedTransactionInfo>;
3939
/** Model of selected reports */
4040
type SelectedReports = {
4141
reportID: string;
42-
policyID: string;
42+
policyID: string | undefined;
4343
action: ValueOf<typeof CONST.SEARCH.ACTION_TYPES>;
4444
total: number;
4545
};
@@ -65,7 +65,8 @@ type SearchContext = {
6565
selectedReports: SelectedReports[];
6666
setCurrentSearchHash: (hash: number) => void;
6767
setSelectedTransactions: (selectedTransactions: SelectedTransactions, data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[]) => void;
68-
clearSelectedTransactions: (hash?: number) => void;
68+
clearSelectedTransactions: (hash?: number, shouldTurnOffSelectionMode?: boolean) => void;
69+
shouldTurnOffSelectionMode: boolean;
6970
shouldShowStatusBarLoading: boolean;
7071
setShouldShowStatusBarLoading: (shouldShow: boolean) => void;
7172
setLastSearchType: (type: string | undefined) => void;

src/libs/PolicyUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1122,7 +1122,7 @@ function getWorkspaceAccountID(policyID?: string) {
11221122
return policy.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID;
11231123
}
11241124

1125-
function hasVBBA(policyID: string) {
1125+
function hasVBBA(policyID: string | undefined) {
11261126
const policy = getPolicy(policyID);
11271127
return !!policy?.achAccount?.bankAccountID;
11281128
}

src/pages/Search/SearchPageBottomTab.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Animated, {clamp, useAnimatedScrollHandler, useAnimatedStyle, useSharedVa
55
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
66
import ScreenWrapper from '@components/ScreenWrapper';
77
import Search from '@components/Search';
8+
import {useSearchContext} from '@components/Search/SearchContext';
89
import SearchStatusBar from '@components/Search/SearchStatusBar';
910
import useActiveCentralPaneRoute from '@hooks/useActiveCentralPaneRoute';
1011
import useLocalize from '@hooks/useLocalize';
@@ -22,6 +23,7 @@ import ROUTES from '@src/ROUTES';
2223
import SCREENS from '@src/SCREENS';
2324
import SearchSelectionModeHeader from './SearchSelectionModeHeader';
2425
import SearchTypeMenu from './SearchTypeMenu';
26+
import useHandleBackButton from './useHandleBackButton';
2527

2628
const TOO_CLOSE_TO_TOP_DISTANCE = 10;
2729
const TOO_CLOSE_TO_BOTTOM_DISTANCE = 10;
@@ -35,6 +37,17 @@ function SearchPageBottomTab() {
3537
const styles = useThemeStyles();
3638
const StyleUtils = useStyleUtils();
3739
const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE);
40+
const {clearSelectedTransactions} = useSearchContext();
41+
42+
const handleBackButtonPress = useCallback(() => {
43+
if (!selectionMode?.isEnabled) {
44+
return false;
45+
}
46+
clearSelectedTransactions(undefined, true);
47+
return true;
48+
}, [selectionMode, clearSelectedTransactions]);
49+
50+
useHandleBackButton(handleBackButtonPress);
3851

3952
const scrollOffset = useSharedValue(0);
4053
const topBarOffset = useSharedValue<number>(StyleUtils.searchHeaderHeight);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {useEffect} from 'react';
2+
import {BackHandler} from 'react-native';
3+
import type UseHandleBackButtonCallback from './type';
4+
5+
export default function useHandleBackButton(callback: UseHandleBackButtonCallback) {
6+
useEffect(() => {
7+
const backHandler = BackHandler.addEventListener('hardwareBackPress', callback);
8+
9+
return () => backHandler.remove();
10+
}, [callback]);
11+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import type UseHandleBackButtonCallback from './type';
2+
3+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
4+
export default function useHandleBackButton(_callback: UseHandleBackButtonCallback) {}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
type UseHandleBackButtonCallback = () => boolean;
2+
3+
export default UseHandleBackButtonCallback;

0 commit comments

Comments
 (0)