Skip to content

Commit 3b65927

Browse files
authored
Merge pull request Expensify#66830 from s77rt/search-total-footer
Search: Add total spend footer
2 parents fef79e8 + a0c54cb commit 3b65927

25 files changed

Lines changed: 229 additions & 182 deletions

src/CONST/index.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6421,20 +6421,6 @@ const CONST = {
64216421
ONYXKEYS.PERSONAL_DETAILS_LIST,
64226422
ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS,
64236423
],
6424-
SEARCH_LIST: {
6425-
EXPENSES: 'expenses',
6426-
REPORTS: 'reports',
6427-
CHATS: 'chats',
6428-
SUBMIT: 'submit',
6429-
APPROVE: 'approve',
6430-
PAY: 'pay',
6431-
EXPORT: 'export',
6432-
STATEMENTS: 'statements',
6433-
UNAPPROVED_CASH: 'unapprovedCash',
6434-
UNAPPROVED_COMPANY_CARDS: 'unapprovedCompanyCards',
6435-
UNAPPROVED_CASH_ONLY: 'unapprovedCashOnly',
6436-
UNAPPROVED_COMPANY_CARDS_ONLY: 'unapprovedCompanyCardsOnly',
6437-
},
64386424
SEARCH_KEYS: {
64396425
EXPENSES: 'expenses',
64406426
REPORTS: 'reports',

src/components/Search/SearchContext.tsx

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,15 @@
11
import React, {useCallback, useContext, useMemo, useRef, useState} from 'react';
2-
import useCardFeedsForDisplay from '@hooks/useCardFeedsForDisplay';
3-
import useOnyx from '@hooks/useOnyx';
42
import {isMoneyRequestReport} from '@libs/ReportUtils';
5-
import {
6-
getSuggestedSearches,
7-
isTransactionCardGroupListItemType,
8-
isTransactionListItemType,
9-
isTransactionMemberGroupListItemType,
10-
isTransactionReportGroupListItemType,
11-
} from '@libs/SearchUIUtils';
3+
import {isTransactionCardGroupListItemType, isTransactionListItemType, isTransactionMemberGroupListItemType, isTransactionReportGroupListItemType} from '@libs/SearchUIUtils';
4+
import type {SearchKey} from '@libs/SearchUIUtils';
125
import CONST from '@src/CONST';
13-
import ONYXKEYS from '@src/ONYXKEYS';
146
import type ChildrenProps from '@src/types/utils/ChildrenProps';
157
import {isEmptyObject} from '@src/types/utils/EmptyObject';
168
import type {SearchContext, SearchContextData, SelectedTransactions} from './types';
179

1810
const defaultSearchContextData: SearchContextData = {
1911
currentSearchHash: -1,
12+
currentSearchKey: undefined,
2013
selectedTransactions: {},
2114
selectedTransactionIDs: [],
2215
selectedReports: [],
@@ -26,13 +19,12 @@ const defaultSearchContextData: SearchContextData = {
2619

2720
const defaultSearchContext: SearchContext = {
2821
...defaultSearchContextData,
29-
currentSearchKey: undefined,
3022
lastSearchType: undefined,
3123
isExportMode: false,
3224
shouldShowExportModeOption: false,
3325
shouldShowFiltersBarLoading: false,
3426
setLastSearchType: () => {},
35-
setCurrentSearchHash: () => {},
27+
setCurrentSearchHashAndKey: () => {},
3628
setSelectedTransactions: () => {},
3729
removeTransaction: () => {},
3830
clearSelectedTransactions: () => {},
@@ -50,25 +42,17 @@ function SearchContextProvider({children}: ChildrenProps) {
5042
const [lastSearchType, setLastSearchType] = useState<string | undefined>(undefined);
5143
const [searchContextData, setSearchContextData] = useState(defaultSearchContextData);
5244
const areTransactionsEmpty = useRef(true);
53-
const {defaultCardFeed} = useCardFeedsForDisplay();
54-
55-
const [accountID] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: (s) => s?.accountID});
56-
const suggestedSearches = useMemo(() => getSuggestedSearches(defaultCardFeed?.id, accountID), [defaultCardFeed?.id, accountID]);
57-
58-
const currentSearchKey = useMemo(() => {
59-
const currentSearch = Object.values(suggestedSearches).find((search) => search.hash === searchContextData.currentSearchHash);
60-
return currentSearch?.key;
61-
}, [suggestedSearches, searchContextData.currentSearchHash]);
6245

63-
const setCurrentSearchHash = useCallback((searchHash: number) => {
46+
const setCurrentSearchHashAndKey = useCallback((searchHash: number, searchKey: SearchKey | undefined) => {
6447
setSearchContextData((prevState) => {
65-
if (searchHash === prevState.currentSearchHash) {
48+
if (searchHash === prevState.currentSearchHash && searchKey === prevState.currentSearchKey) {
6649
return prevState;
6750
}
6851

6952
return {
7053
...prevState,
7154
currentSearchHash: searchHash,
55+
currentSearchKey: searchKey,
7256
};
7357
});
7458
}, []);
@@ -190,9 +174,8 @@ function SearchContextProvider({children}: ChildrenProps) {
190174
const searchContext = useMemo<SearchContext>(
191175
() => ({
192176
...searchContextData,
193-
currentSearchKey,
194177
removeTransaction,
195-
setCurrentSearchHash,
178+
setCurrentSearchHashAndKey,
196179
setSelectedTransactions,
197180
clearSelectedTransactions,
198181
shouldShowFiltersBarLoading,
@@ -206,9 +189,8 @@ function SearchContextProvider({children}: ChildrenProps) {
206189
}),
207190
[
208191
searchContextData,
209-
currentSearchKey,
210192
removeTransaction,
211-
setCurrentSearchHash,
193+
setCurrentSearchHashAndKey,
212194
setSelectedTransactions,
213195
clearSelectedTransactions,
214196
shouldShowFiltersBarLoading,
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React, {useMemo} from 'react';
2+
import {View} from 'react-native';
3+
import Text from '@components/Text';
4+
import useLocalize from '@hooks/useLocalize';
5+
import useNetwork from '@hooks/useNetwork';
6+
import useStyleUtils from '@hooks/useStyleUtils';
7+
import useTheme from '@hooks/useTheme';
8+
import useThemeStyles from '@hooks/useThemeStyles';
9+
import {convertToDisplayString} from '@libs/CurrencyUtils';
10+
import type {SearchResultsInfo} from '@src/types/onyx/SearchResults';
11+
12+
type SearchPageFooterProps = {
13+
metadata: SearchResultsInfo;
14+
};
15+
16+
function SearchPageFooter({metadata}: SearchPageFooterProps) {
17+
const theme = useTheme();
18+
const styles = useThemeStyles();
19+
const StyleUtils = useStyleUtils();
20+
const {translate} = useLocalize();
21+
const {isOffline} = useNetwork();
22+
23+
const valueTextStyle = useMemo(() => (isOffline ? [styles.textLabelSupporting, styles.labelStrong] : [styles.labelStrong]), [isOffline, styles]);
24+
25+
return (
26+
<View style={[styles.justifyContentEnd, styles.borderTop, styles.ph5, styles.pv3, styles.flexRow, styles.gap3, StyleUtils.getBackgroundColorStyle(theme.appBG)]}>
27+
<View style={[styles.flexRow, styles.gap1]}>
28+
<Text style={styles.textLabelSupporting}>{`${translate('common.expenses')}:`}</Text>
29+
<Text style={valueTextStyle}>{metadata.count}</Text>
30+
</View>
31+
<View style={[styles.flexRow, styles.gap1]}>
32+
<Text style={styles.textLabelSupporting}>{`${translate('common.totalSpend')}:`}</Text>
33+
<Text style={valueTextStyle}>{convertToDisplayString(metadata.total, metadata.currency)}</Text>
34+
</View>
35+
</View>
36+
);
37+
}
38+
39+
SearchPageFooter.displayName = 'SearchPageFooter';
40+
41+
export default SearchPageFooter;

src/components/Search/index.tsx

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOffli
88
import SearchTableHeader from '@components/SelectionList/SearchTableHeader';
99
import type {ReportActionListItemType, SearchListItem, SelectionListHandle, TransactionGroupListItemType, TransactionListItemType} from '@components/SelectionList/types';
1010
import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
11+
import useCardFeedsForDisplay from '@hooks/useCardFeedsForDisplay';
1112
import useLocalize from '@hooks/useLocalize';
1213
import useNetwork from '@hooks/useNetwork';
1314
import useOnyx from '@hooks/useOnyx';
@@ -30,6 +31,7 @@ import {
3031
getListItem,
3132
getSections,
3233
getSortedSections,
34+
getSuggestedSearches,
3335
getWideAmountIndicators,
3436
isReportActionListItemType,
3537
isSearchDataLoaded,
@@ -40,6 +42,7 @@ import {
4042
shouldShowEmptyState,
4143
shouldShowYear as shouldShowYearUtil,
4244
} from '@libs/SearchUIUtils';
45+
import type {SearchKey} from '@libs/SearchUIUtils';
4346
import {isOnHold, isTransactionPendingDelete} from '@libs/TransactionUtils';
4447
import Navigation from '@navigation/Navigation';
4548
import type {SearchFullscreenNavigatorParamList} from '@navigation/types';
@@ -147,8 +150,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
147150
const navigation = useNavigation<PlatformStackNavigationProp<SearchFullscreenNavigatorParamList>>();
148151
const isFocused = useIsFocused();
149152
const {
150-
currentSearchKey,
151-
setCurrentSearchHash,
153+
setCurrentSearchHashAndKey,
152154
setSelectedTransactions,
153155
selectedTransactions,
154156
clearSelectedTransactions,
@@ -161,8 +163,6 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
161163
} = useSearchContext();
162164
const [offset, setOffset] = useState(0);
163165

164-
const {type, status, sortBy, sortOrder, hash, groupBy} = queryJSON;
165-
166166
const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: true});
167167
const previousTransactions = usePrevious(transactions);
168168
const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {canBeMissing: true});
@@ -179,6 +179,23 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
179179
);
180180
},
181181
});
182+
const {defaultCardFeed} = useCardFeedsForDisplay();
183+
const [accountID] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: (s) => s?.accountID});
184+
const suggestedSearches = useMemo(() => getSuggestedSearches(defaultCardFeed?.id, accountID), [defaultCardFeed?.id, accountID]);
185+
186+
const {type, status, sortBy, sortOrder, hash, groupBy} = queryJSON;
187+
const searchKey = useMemo(() => Object.values(suggestedSearches).find((search) => search.hash === hash)?.key, [suggestedSearches, hash]);
188+
189+
const shouldCalculateTotals = useMemo(() => {
190+
if (offset !== 0) {
191+
return false;
192+
}
193+
if (!searchKey) {
194+
return false;
195+
}
196+
const eligibleSearchKeys: Partial<SearchKey[]> = [CONST.SEARCH.SEARCH_KEYS.STATEMENTS, CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH, CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_COMPANY_CARDS];
197+
return eligibleSearchKeys.includes(searchKey);
198+
}, [offset, searchKey]);
182199

183200
const previousReportActions = usePrevious(reportActions);
184201
const reportActionsArray = useMemo(
@@ -194,8 +211,8 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
194211
useFocusEffect(
195212
useCallback(() => {
196213
clearSelectedTransactions(hash);
197-
setCurrentSearchHash(hash);
198-
}, [hash, clearSelectedTransactions, setCurrentSearchHash]),
214+
setCurrentSearchHashAndKey(hash, searchKey);
215+
}, [hash, searchKey, clearSelectedTransactions, setCurrentSearchHashAndKey]),
199216
);
200217

201218
const isSearchResultsEmpty = !searchResults?.data || isSearchResultsEmptyUtil(searchResults);
@@ -205,7 +222,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
205222
return;
206223
}
207224

208-
const selectedKeys = Object.keys(selectedTransactions).filter((key) => selectedTransactions[key]);
225+
const selectedKeys = Object.keys(selectedTransactions).filter((transactionKey) => selectedTransactions[transactionKey]);
209226
if (selectedKeys.length === 0 && isMobileSelectionModeEnabled && shouldTurnOffSelectionMode) {
210227
turnOffMobileSelectionMode();
211228
}
@@ -220,7 +237,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
220237
return;
221238
}
222239

223-
const selectedKeys = Object.keys(selectedTransactions).filter((key) => selectedTransactions[key]);
240+
const selectedKeys = Object.keys(selectedTransactions).filter((transactionKey) => selectedTransactions[transactionKey]);
224241
if (!isSmallScreenWidth) {
225242
if (selectedKeys.length === 0 && isMobileSelectionModeEnabled) {
226243
turnOffMobileSelectionMode();
@@ -239,11 +256,11 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
239256
return;
240257
}
241258

242-
handleSearch({queryJSON, offset});
259+
handleSearch({queryJSON, searchKey, offset, shouldCalculateTotals});
243260
// We don't need to run the effect on change of isFocused.
244261
// eslint-disable-next-line react-compiler/react-compiler
245262
// eslint-disable-next-line react-hooks/exhaustive-deps
246-
}, [handleSearch, isOffline, offset, queryJSON]);
263+
}, [handleSearch, isOffline, offset, queryJSON, searchKey, shouldCalculateTotals]);
247264

248265
useEffect(() => {
249266
openSearch();
@@ -254,7 +271,9 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
254271
transactions,
255272
previousTransactions,
256273
queryJSON,
274+
searchKey,
257275
offset,
276+
shouldCalculateTotals,
258277
reportActions,
259278
previousReportActions,
260279
});
@@ -279,8 +298,8 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
279298
return [];
280299
}
281300

282-
return getSections(type, searchResults.data, searchResults.search, groupBy, exportReportActions, currentSearchKey);
283-
}, [currentSearchKey, exportReportActions, groupBy, isDataLoaded, searchResults, type]);
301+
return getSections(type, searchResults.data, searchResults.search, groupBy, exportReportActions, searchKey);
302+
}, [searchKey, exportReportActions, groupBy, isDataLoaded, searchResults, type]);
284303

285304
useEffect(() => {
286305
/** We only want to display the skeleton for the status filters the first time we load them for a specific data type */

src/components/Search/types.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type {ValueOf} from 'type-fest';
22
import type {ReportActionListItemType, TaskListItemType, TransactionGroupListItemType, TransactionListItemType} from '@components/SelectionList/types';
3-
import type {SuggestedSearchKey} from '@libs/SearchUIUtils';
3+
import type {SearchKey} from '@libs/SearchUIUtils';
44
import type CONST from '@src/CONST';
55
import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
66

@@ -70,6 +70,7 @@ type SearchDatePreset = ValueOf<typeof CONST.SEARCH.DATE_PRESETS>;
7070

7171
type SearchContextData = {
7272
currentSearchHash: number;
73+
currentSearchKey: SearchKey | undefined;
7374
selectedTransactions: SelectedTransactions;
7475
selectedTransactionIDs: string[];
7576
selectedReports: SelectedReports[];
@@ -78,8 +79,7 @@ type SearchContextData = {
7879
};
7980

8081
type SearchContext = SearchContextData & {
81-
currentSearchKey: SuggestedSearchKey | undefined;
82-
setCurrentSearchHash: (hash: number) => void;
82+
setCurrentSearchHashAndKey: (hash: number, key: SearchKey | undefined) => void;
8383
/** If you want to set `selectedTransactionIDs`, pass an array as the first argument, object/record otherwise */
8484
setSelectedTransactions: {
8585
(selectedTransactionIDs: string[], unused?: undefined): void;
@@ -169,7 +169,9 @@ type SearchAutocompleteQueryRange = {
169169

170170
type SearchParams = {
171171
queryJSON: SearchQueryJSON;
172+
searchKey: SearchKey | undefined;
172173
offset: number;
174+
shouldCalculateTotals: boolean;
173175
};
174176

175177
export type {

src/hooks/useSearchHighlightAndScroll.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {SearchQueryJSON} from '@components/Search/types';
55
import type {SearchListItem, SelectionListHandle, TransactionGroupListItemType, TransactionListItemType} from '@components/SelectionList/types';
66
import {search} from '@libs/actions/Search';
77
import {isReportActionEntry} from '@libs/SearchUIUtils';
8+
import type {SearchKey} from '@libs/SearchUIUtils';
89
import CONST from '@src/CONST';
910
import ONYXKEYS from '@src/ONYXKEYS';
1011
import type {ReportActions, SearchResults, Transaction} from '@src/types/onyx';
@@ -17,13 +18,25 @@ type UseSearchHighlightAndScroll = {
1718
reportActions: OnyxCollection<ReportActions>;
1819
previousReportActions: OnyxCollection<ReportActions>;
1920
queryJSON: SearchQueryJSON;
21+
searchKey: SearchKey | undefined;
2022
offset: number;
23+
shouldCalculateTotals: boolean;
2124
};
2225

2326
/**
2427
* Hook used to trigger a search when a new transaction or report action is added and handle highlighting and scrolling.
2528
*/
26-
function useSearchHighlightAndScroll({searchResults, transactions, previousTransactions, reportActions, previousReportActions, queryJSON, offset}: UseSearchHighlightAndScroll) {
29+
function useSearchHighlightAndScroll({
30+
searchResults,
31+
transactions,
32+
previousTransactions,
33+
reportActions,
34+
previousReportActions,
35+
queryJSON,
36+
searchKey,
37+
offset,
38+
shouldCalculateTotals,
39+
}: UseSearchHighlightAndScroll) {
2740
const isFocused = useIsFocused();
2841
// Ref to track if the search was triggered by this hook
2942
const triggeredByHookRef = useRef(false);
@@ -97,12 +110,25 @@ function useSearchHighlightAndScroll({searchResults, transactions, previousTrans
97110
triggeredByHookRef.current = true;
98111

99112
// Trigger the search
100-
search({queryJSON, offset});
113+
search({queryJSON, searchKey, offset, shouldCalculateTotals});
101114

102115
// Set the ref to prevent further triggers until reset
103116
searchTriggeredRef.current = true;
104117
}
105-
}, [isFocused, transactions, previousTransactions, queryJSON, offset, reportActions, previousReportActions, isChat, searchResults?.data, existingSearchResultIDs]);
118+
}, [
119+
isFocused,
120+
transactions,
121+
previousTransactions,
122+
queryJSON,
123+
searchKey,
124+
offset,
125+
shouldCalculateTotals,
126+
reportActions,
127+
previousReportActions,
128+
isChat,
129+
searchResults?.data,
130+
existingSearchResultIDs,
131+
]);
106132

107133
// Initialize the set with existing IDs only once
108134
useEffect(() => {

0 commit comments

Comments
 (0)