Skip to content

Commit d2e5e9f

Browse files
authored
Merge pull request #79178 from ShridharGoel/issue78149
Update search total amount footer logic
2 parents 462dc1c + 15809e6 commit d2e5e9f

File tree

6 files changed

+135
-27
lines changed

6 files changed

+135
-27
lines changed

src/components/MoneyReportHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ function MoneyReportHeader({
414414
> | null>(null);
415415

416416
const {selectedTransactionIDs, removeTransaction, clearSelectedTransactions, currentSearchQueryJSON, currentSearchKey, currentSearchHash, currentSearchResults} = useSearchContext();
417-
const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.similarSearchHash, true);
417+
const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.hash, true);
418418

419419
const [network] = useOnyx(ONYXKEYS.NETWORK, {canBeMissing: true});
420420

src/components/Search/index.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ function Search({
288288
const suggestedSearches = useMemo(() => getSuggestedSearches(accountID, defaultCardFeed?.id), [defaultCardFeed?.id, accountID]);
289289
const searchKey = useMemo(() => Object.values(suggestedSearches).find((search) => search.similarSearchHash === similarSearchHash)?.key, [suggestedSearches, similarSearchHash]);
290290
const searchDataType = useMemo(() => (shouldUseLiveData ? CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT : searchResults?.search?.type), [shouldUseLiveData, searchResults?.search?.type]);
291-
const shouldCalculateTotals = useSearchShouldCalculateTotals(searchKey, similarSearchHash, offset === 0);
291+
const shouldCalculateTotals = useSearchShouldCalculateTotals(searchKey, hash, offset === 0);
292292

293293
const previousReportActions = usePrevious(reportActions);
294294
const {translate, localeCompare, formatPhoneNumber} = useLocalize();
@@ -546,6 +546,8 @@ function Search({
546546
setShouldShowFiltersBarLoading(shouldShowLoadingState && lastSearchType !== type);
547547
}, [lastSearchType, setShouldShowFiltersBarLoading, shouldShowLoadingState, type]);
548548

549+
const shouldRetrySearchWithTotalsRef = useRef(false);
550+
549551
useEffect(() => {
550552
const focusedRoute = findFocusedRoute(navigationRef.getRootState());
551553
const isMigratedModalDisplayed = focusedRoute?.name === NAVIGATORS.MIGRATED_USER_MODAL_NAVIGATOR || focusedRoute?.name === SCREENS.MIGRATED_USER_WELCOME_MODAL.ROOT;
@@ -555,12 +557,34 @@ function Search({
555557
return;
556558
}
557559

560+
if (searchResults?.search?.isLoading) {
561+
if (shouldCalculateTotals && searchResults?.search?.count === undefined) {
562+
shouldRetrySearchWithTotalsRef.current = true;
563+
}
564+
return;
565+
}
566+
558567
handleSearch({queryJSON, searchKey, offset, shouldCalculateTotals, prevReportsLength: filteredDataLength, isLoading: !!searchResults?.search?.isLoading});
559568

560569
// We don't need to run the effect on change of isFocused.
561570
// eslint-disable-next-line react-hooks/exhaustive-deps
562571
}, [handleSearch, isOffline, offset, queryJSON, searchKey, shouldCalculateTotals]);
563572

573+
useEffect(() => {
574+
if (!shouldRetrySearchWithTotalsRef.current || searchResults?.search?.isLoading || !shouldCalculateTotals) {
575+
return;
576+
}
577+
578+
// If count is already present, the latest response already contains totals and we can skip the re-query.
579+
if (searchResults?.search?.count !== undefined) {
580+
shouldRetrySearchWithTotalsRef.current = false;
581+
return;
582+
}
583+
584+
shouldRetrySearchWithTotalsRef.current = false;
585+
handleSearch({queryJSON, searchKey, offset, shouldCalculateTotals: true, prevReportsLength: filteredDataLength, isLoading: false});
586+
}, [filteredDataLength, handleSearch, offset, queryJSON, searchKey, searchResults?.search?.count, searchResults?.search?.isLoading, shouldCalculateTotals]);
587+
564588
// When new data load, selectedTransactions is updated in next effect. We use this flag to whether selection is updated
565589
const isRefreshingSelection = useRef(false);
566590

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
import {useMemo} from 'react';
2-
import {buildSearchQueryJSON} from '@libs/SearchQueryUtils';
32
import type {SearchKey} from '@libs/SearchUIUtils';
43
import CONST from '@src/CONST';
54
import ONYXKEYS from '@src/ONYXKEYS';
65
import useOnyx from './useOnyx';
76

8-
function useSearchShouldCalculateTotals(searchKey: SearchKey | undefined, similarSearchHash: number | undefined, enabled: boolean) {
7+
function useSearchShouldCalculateTotals(searchKey: SearchKey | undefined, searchHash: number | undefined, enabled: boolean) {
98
const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true});
109

1110
const shouldCalculateTotals = useMemo(() => {
1211
if (!enabled) {
1312
return false;
1413
}
1514

16-
const savedSearchValues = Object.values(savedSearches ?? {});
17-
18-
if (!savedSearchValues.length && !searchKey) {
15+
if (!Object.keys(savedSearches ?? {}).length && !searchKey) {
1916
return false;
2017
}
2118

@@ -30,21 +27,13 @@ function useSearchShouldCalculateTotals(searchKey: SearchKey | undefined, simila
3027
CONST.SEARCH.SEARCH_KEYS.RECONCILIATION,
3128
];
3229

33-
if (eligibleSearchKeys.includes(searchKey)) {
34-
return true;
35-
}
36-
37-
for (const savedSearch of savedSearchValues) {
38-
const searchData = buildSearchQueryJSON(savedSearch.query);
39-
if (searchData && searchData.similarSearchHash === similarSearchHash) {
40-
return true;
41-
}
42-
}
30+
const isSuggestedSearchWithTotals = eligibleSearchKeys.includes(searchKey);
31+
const isSavedSearch = searchHash !== undefined && savedSearches && !!savedSearches[searchHash];
4332

44-
return false;
45-
}, [enabled, savedSearches, searchKey, similarSearchHash]);
33+
return isSuggestedSearchWithTotals || isSavedSearch;
34+
}, [enabled, savedSearches, searchKey, searchHash]);
4635

47-
return shouldCalculateTotals;
36+
return shouldCalculateTotals ?? false;
4837
}
4938

5039
export default useSearchShouldCalculateTotals;

src/pages/Search/SearchPage.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import useOnyx from '@hooks/useOnyx';
3434
import usePermissions from '@hooks/usePermissions';
3535
import usePrevious from '@hooks/usePrevious';
3636
import useResponsiveLayout from '@hooks/useResponsiveLayout';
37+
import useSearchShouldCalculateTotals from '@hooks/useSearchShouldCalculateTotals';
3738
import useSelfDMReport from '@hooks/useSelfDMReport';
3839
import useTheme from '@hooks/useTheme';
3940
import useThemeStyles from '@hooks/useThemeStyles';
@@ -109,8 +110,17 @@ function SearchPage({route}: SearchPageProps) {
109110
const theme = useTheme();
110111
const {isOffline} = useNetwork();
111112
const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext);
112-
const {selectedTransactions, clearSelectedTransactions, selectedReports, lastSearchType, setLastSearchType, areAllMatchingItemsSelected, selectAllMatchingItems, currentSearchResults} =
113-
useSearchContext();
113+
const {
114+
selectedTransactions,
115+
clearSelectedTransactions,
116+
selectedReports,
117+
lastSearchType,
118+
setLastSearchType,
119+
areAllMatchingItemsSelected,
120+
selectAllMatchingItems,
121+
currentSearchKey,
122+
currentSearchResults,
123+
} = useSearchContext();
114124
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
115125
const isMobileSelectionModeEnabled = useMobileSelectionMode(clearSelectedTransactions);
116126
const allTransactions = useAllTransactions();
@@ -1187,7 +1197,8 @@ function SearchPage({route}: SearchPageProps) {
11871197
const {resetVideoPlayerData} = usePlaybackActionsContext();
11881198

11891199
const metadata = searchResults?.search;
1190-
const shouldShowFooter = !!metadata?.count || selectedTransactionsKeys.length > 0;
1200+
const shouldAllowFooterTotals = useSearchShouldCalculateTotals(currentSearchKey, queryJSON?.hash, true);
1201+
const shouldShowFooter = selectedTransactionsKeys.length > 0 || (shouldAllowFooterTotals && !!metadata?.count);
11911202

11921203
// Handles video player cleanup:
11931204
// 1. On mount: Resets player if navigating from report screen
@@ -1229,7 +1240,11 @@ function SearchPage({route}: SearchPageProps) {
12291240
}, []);
12301241

12311242
const footerData = useMemo(() => {
1232-
const shouldUseClientTotal = !metadata?.count || (selectedTransactionsKeys.length > 0 && !areAllMatchingItemsSelected);
1243+
if (!shouldAllowFooterTotals && selectedTransactionsKeys.length === 0) {
1244+
return {count: undefined, total: undefined, currency: undefined};
1245+
}
1246+
1247+
const shouldUseClientTotal = selectedTransactionsKeys.length > 0 || !metadata?.count || (selectedTransactionsKeys.length > 0 && !areAllMatchingItemsSelected);
12331248
const selectedTransactionItems = Object.values(selectedTransactions);
12341249
const currency = metadata?.currency ?? selectedTransactionItems.at(0)?.groupCurrency;
12351250
const numberOfExpense = shouldUseClientTotal
@@ -1245,7 +1260,7 @@ function SearchPage({route}: SearchPageProps) {
12451260
const total = shouldUseClientTotal ? selectedTransactionItems.reduce((acc, transaction) => acc - (transaction.groupAmount ?? 0), 0) : metadata?.total;
12461261

12471262
return {count: numberOfExpense, total, currency};
1248-
}, [areAllMatchingItemsSelected, metadata?.count, metadata?.currency, metadata?.total, selectedTransactions, selectedTransactionsKeys]);
1263+
}, [areAllMatchingItemsSelected, metadata?.count, metadata?.currency, metadata?.total, selectedTransactions, selectedTransactionsKeys, shouldAllowFooterTotals]);
12491264

12501265
const onSortPressedCallback = useCallback(() => {
12511266
setIsSorting(true);
@@ -1308,6 +1323,7 @@ function SearchPage({route}: SearchPageProps) {
13081323
currentSelectedReportID={selectedTransactionReportIDs?.at(0) ?? selectedReportIDs?.at(0)}
13091324
confirmPayment={onBulkPaySelected}
13101325
latestBankItems={latestBankItems}
1326+
shouldShowFooter={shouldShowFooter}
13111327
/>
13121328
<DragAndDropConsumer onDrop={initScanRequest}>
13131329
<DropZoneUI

src/pages/Search/SearchPageNarrow.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type SearchPageNarrowProps = {
5858
currentSelectedReportID?: string | undefined;
5959
confirmPayment?: (paymentType: PaymentMethodType | undefined) => void;
6060
latestBankItems?: BankAccountMenuItem[] | undefined;
61+
shouldShowFooter: boolean;
6162
};
6263

6364
function SearchPageNarrow({
@@ -71,13 +72,14 @@ function SearchPageNarrow({
7172
currentSelectedReportID,
7273
latestBankItems,
7374
confirmPayment,
75+
shouldShowFooter,
7476
}: SearchPageNarrowProps) {
7577
const {translate} = useLocalize();
7678
const {shouldUseNarrowLayout} = useResponsiveLayout();
7779
const {windowHeight} = useWindowDimensions();
7880
const styles = useThemeStyles();
7981
const StyleUtils = useStyleUtils();
80-
const {clearSelectedTransactions, selectedTransactions} = useSearchContext();
82+
const {clearSelectedTransactions} = useSearchContext();
8183
const [searchRouterListVisible, setSearchRouterListVisible] = useState(false);
8284
const {isOffline} = useNetwork();
8385
// Controls the visibility of the educational tooltip based on user scrolling.
@@ -176,7 +178,6 @@ function SearchPageNarrow({
176178
);
177179
}
178180

179-
const shouldShowFooter = !!metadata?.count || Object.keys(selectedTransactions).length > 0;
180181
const isDataLoaded = isSearchDataLoaded(searchResults, queryJSON);
181182
const shouldShowLoadingState = !isOffline && (!isDataLoaded || !!metadata?.isLoading);
182183

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import {renderHook} from '@testing-library/react-native';
2+
import useSearchShouldCalculateTotals from '@hooks/useSearchShouldCalculateTotals';
3+
import CONST from '@src/CONST';
4+
import ONYXKEYS from '@src/ONYXKEYS';
5+
6+
const onyxData: Record<string, unknown> = {};
7+
8+
const mockUseOnyx = jest.fn(
9+
(
10+
key: string,
11+
options?: {
12+
selector?: (value: unknown) => unknown;
13+
},
14+
) => {
15+
const value = onyxData[key];
16+
const selectedValue = options?.selector ? options.selector(value as never) : value;
17+
return [selectedValue];
18+
},
19+
);
20+
21+
jest.mock('@hooks/useOnyx', () => ({
22+
// eslint-disable-next-line @typescript-eslint/naming-convention
23+
__esModule: true,
24+
default: (key: string, options?: {selector?: (value: unknown) => unknown}) => mockUseOnyx(key, options),
25+
}));
26+
27+
describe('useSearchShouldCalculateTotals', () => {
28+
beforeEach(() => {
29+
onyxData[ONYXKEYS.SAVED_SEARCHES] = undefined;
30+
mockUseOnyx.mockClear();
31+
});
32+
33+
it('returns false when disabled', () => {
34+
const {result} = renderHook(() => useSearchShouldCalculateTotals(CONST.SEARCH.SEARCH_KEYS.SUBMIT, 123, false));
35+
36+
expect(result.current).toBe(false);
37+
});
38+
39+
it('returns true for eligible suggested searches', () => {
40+
const {result} = renderHook(() => useSearchShouldCalculateTotals(CONST.SEARCH.SEARCH_KEYS.SUBMIT, 123, true));
41+
42+
expect(result.current).toBe(true);
43+
});
44+
45+
it('returns false for non-eligible searches', () => {
46+
const {result} = renderHook(() => useSearchShouldCalculateTotals(CONST.SEARCH.SEARCH_KEYS.EXPENSES, 123, true));
47+
48+
expect(result.current).toBe(false);
49+
});
50+
51+
it('returns true for saved searches that match the hash', () => {
52+
onyxData[ONYXKEYS.SAVED_SEARCHES] = {
53+
// eslint-disable-next-line @typescript-eslint/naming-convention
54+
456: {
55+
name: 'My search',
56+
query: 'type:expense',
57+
},
58+
};
59+
60+
const {result} = renderHook(() => useSearchShouldCalculateTotals(undefined, 456, true));
61+
62+
expect(result.current).toBe(true);
63+
});
64+
65+
it('returns false when saved searches do not match the hash', () => {
66+
onyxData[ONYXKEYS.SAVED_SEARCHES] = {
67+
// eslint-disable-next-line @typescript-eslint/naming-convention
68+
456: {
69+
name: 'My search',
70+
query: 'type:expense',
71+
},
72+
};
73+
74+
const {result} = renderHook(() => useSearchShouldCalculateTotals(undefined, 789, true));
75+
76+
expect(result.current).toBe(false);
77+
});
78+
});

0 commit comments

Comments
 (0)