Skip to content

Commit e3cb6c8

Browse files
authored
Merge pull request Expensify#79506 from Expensify/cmartins-updateTodosLiveData
Use live data for todos
2 parents a018987 + 3f79b84 commit e3cb6c8

11 files changed

Lines changed: 266 additions & 25 deletions

src/components/Search/SearchContext.tsx

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,34 @@
11
import React, {useCallback, useContext, useMemo, useRef, useState} from 'react';
2-
import useOnyx from '@hooks/useOnyx';
2+
// We need direct access to useOnyx from react-native-onyx to avoid circular dependencies in SearchContext
3+
// eslint-disable-next-line no-restricted-imports
4+
import {useOnyx} from 'react-native-onyx';
5+
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
6+
import useTodos from '@hooks/useTodos';
37
import {isMoneyRequestReport} from '@libs/ReportUtils';
4-
import {isTransactionListItemType, isTransactionReportGroupListItemType} from '@libs/SearchUIUtils';
8+
import {getSuggestedSearches, isTodoSearch, isTransactionListItemType, isTransactionReportGroupListItemType} from '@libs/SearchUIUtils';
59
import type {SearchKey} from '@libs/SearchUIUtils';
610
import CONST from '@src/CONST';
711
import ONYXKEYS from '@src/ONYXKEYS';
12+
import type {SearchResults} from '@src/types/onyx';
13+
import type {SearchResultsInfo} from '@src/types/onyx/SearchResults';
814
import type ChildrenProps from '@src/types/utils/ChildrenProps';
915
import {isEmptyObject} from '@src/types/utils/EmptyObject';
1016
import type {SearchContextData, SearchContextProps, SearchQueryJSON, SelectedTransactions} from './types';
1117

18+
// Default search info when building from live data
19+
// Used for to-do searches where we build SearchResults from live Onyx data instead of API snapshots
20+
const defaultSearchInfo: SearchResultsInfo = {
21+
offset: 0,
22+
type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT,
23+
status: CONST.SEARCH.STATUS.EXPENSE.ALL,
24+
hasMoreResults: false,
25+
hasResults: true,
26+
isLoading: false,
27+
count: 0,
28+
total: 0,
29+
currency: '',
30+
};
31+
1232
const defaultSearchContextData: SearchContextData = {
1333
currentSearchHash: -1,
1434
currentSearchKey: undefined,
@@ -29,6 +49,7 @@ const defaultSearchContext: SearchContextProps = {
2949
showSelectAllMatchingItems: false,
3050
shouldShowFiltersBarLoading: false,
3151
currentSearchResults: undefined,
52+
shouldUseLiveData: false,
3253
setLastSearchType: () => {},
3354
setCurrentSearchHashAndKey: () => {},
3455
setCurrentSearchQueryJSON: () => {},
@@ -51,7 +72,37 @@ function SearchContextProvider({children}: ChildrenProps) {
5172
const [searchContextData, setSearchContextData] = useState(defaultSearchContextData);
5273
const areTransactionsEmpty = useRef(true);
5374

54-
const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${searchContextData.currentSearchHash}`, {canBeMissing: true});
75+
const [snapshotSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${searchContextData.currentSearchHash}`, {canBeMissing: true});
76+
const {todoSearchResultsData} = useTodos();
77+
78+
const currentSearchKey = searchContextData.currentSearchKey;
79+
const currentSearchHash = searchContextData.currentSearchHash;
80+
const {accountID} = useCurrentUserPersonalDetails();
81+
const suggestedSearches = useMemo(() => getSuggestedSearches(accountID), [accountID]);
82+
const shouldUseLiveData = !!currentSearchKey && isTodoSearch(currentSearchHash, suggestedSearches);
83+
84+
// If viewing a to-do search, use live data from useTodos, otherwise return the snapshot data
85+
// We do this so that we can show the counters for the to-do search results without visiting the specific to-do page, e.g. show `Approve [3]` while viewing the `Submit` to-do search.
86+
const currentSearchResults = useMemo((): SearchResults | undefined => {
87+
if (shouldUseLiveData) {
88+
const liveData = todoSearchResultsData[currentSearchKey as keyof typeof todoSearchResultsData];
89+
const searchInfo: SearchResultsInfo = {
90+
...(snapshotSearchResults?.search ?? defaultSearchInfo),
91+
count: liveData.metadata.count,
92+
total: liveData.metadata.total,
93+
currency: liveData.metadata.currency,
94+
};
95+
const hasResults = Object.keys(liveData.data).length > 0;
96+
// For to-do searches, always return a valid SearchResults object (even with empty data)
97+
// This ensures we show the empty state instead of loading/blocking views
98+
return {
99+
search: {...searchInfo, isLoading: false, hasResults},
100+
data: liveData.data,
101+
};
102+
}
103+
104+
return snapshotSearchResults ?? undefined;
105+
}, [shouldUseLiveData, currentSearchKey, todoSearchResultsData, snapshotSearchResults]);
55106

56107
const setCurrentSearchHashAndKey = useCallback((searchHash: number, searchKey: SearchKey | undefined) => {
57108
setSearchContextData((prevState) => {
@@ -207,6 +258,7 @@ function SearchContextProvider({children}: ChildrenProps) {
207258
() => ({
208259
...searchContextData,
209260
currentSearchResults,
261+
shouldUseLiveData,
210262
removeTransaction,
211263
setCurrentSearchHashAndKey,
212264
setCurrentSearchQueryJSON,
@@ -225,6 +277,7 @@ function SearchContextProvider({children}: ChildrenProps) {
225277
[
226278
searchContextData,
227279
currentSearchResults,
280+
shouldUseLiveData,
228281
removeTransaction,
229282
setCurrentSearchHashAndKey,
230283
setCurrentSearchQueryJSON,

src/components/Search/index.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ function Search({
239239
selectAllMatchingItems,
240240
shouldResetSearchQuery,
241241
setShouldResetSearchQuery,
242+
shouldUseLiveData,
242243
} = useSearchContext();
243244
const [offset, setOffset] = useState(0);
244245

@@ -269,9 +270,8 @@ function Search({
269270

270271
const {defaultCardFeed} = useCardFeedsForDisplay();
271272
const suggestedSearches = useMemo(() => getSuggestedSearches(accountID, defaultCardFeed?.id), [defaultCardFeed?.id, accountID]);
272-
273273
const searchKey = useMemo(() => Object.values(suggestedSearches).find((search) => search.similarSearchHash === similarSearchHash)?.key, [suggestedSearches, similarSearchHash]);
274-
274+
const searchDataType = useMemo(() => (shouldUseLiveData ? CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT : searchResults?.search?.type), [shouldUseLiveData, searchResults?.search?.type]);
275275
const shouldCalculateTotals = useSearchShouldCalculateTotals(searchKey, similarSearchHash, offset === 0);
276276

277277
const previousReportActions = usePrevious(reportActions);
@@ -359,15 +359,18 @@ function Search({
359359
shouldCalculateTotals,
360360
reportActions,
361361
previousReportActions,
362+
shouldUseLiveData,
362363
});
363364

364365
// There's a race condition in Onyx which makes it return data from the previous Search, so in addition to checking that the data is loaded
365366
// we also need to check that the searchResults matches the type and status of the current search
366-
const isDataLoaded = isSearchDataLoaded(searchResults, queryJSON);
367+
const isDataLoaded = shouldUseLiveData || isSearchDataLoaded(searchResults, queryJSON);
367368

368369
const hasErrors = Object.keys(searchResults?.errors ?? {}).length > 0 && !isOffline;
369370

371+
// For to-do searches, we never show loading state since the data is always available locally from Onyx
370372
const shouldShowLoadingState =
373+
!shouldUseLiveData &&
371374
!isOffline &&
372375
(!isDataLoaded || (!!searchResults?.search.isLoading && Array.isArray(searchResults?.data) && searchResults?.data.length === 0) || (hasErrors && !searchRequestResponseStatusCode));
373376
const shouldShowLoadingMoreItems = !shouldShowLoadingState && searchResults?.search?.isLoading && searchResults?.search?.offset > 0;
@@ -866,8 +869,8 @@ function Search({
866869
if (!searchResults?.data) {
867870
return [];
868871
}
869-
return getColumnsToShow(accountID, searchResults?.data, visibleColumns, false, searchResults?.search?.type, validGroupBy);
870-
}, [accountID, searchResults?.data, searchResults?.search?.type, visibleColumns, validGroupBy]);
872+
return getColumnsToShow(accountID, searchResults?.data, visibleColumns, false, searchDataType, validGroupBy);
873+
}, [accountID, searchResults?.data, searchDataType, visibleColumns, validGroupBy]);
871874

872875
const opacity = useSharedValue(1);
873876
const animatedStyle = useAnimatedStyle(() => ({
@@ -1071,7 +1074,7 @@ function Search({
10711074
}
10721075

10731076
const visibleDataLength = filteredData.filter((item) => item.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline).length;
1074-
if (shouldShowEmptyState(isDataLoaded, visibleDataLength, searchResults?.search?.type)) {
1077+
if (shouldShowEmptyState(isDataLoaded, visibleDataLength, searchDataType)) {
10751078
cancelSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB);
10761079
return (
10771080
<View style={[shouldUseNarrowLayout ? styles.searchListContentContainerStyles : styles.mt3, styles.flex1]}>

src/components/Search/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ type SearchContextData = {
139139

140140
type SearchContextProps = SearchContextData & {
141141
currentSearchResults: SearchResults | undefined;
142+
/** Whether we're on a main to-do search and should use live Onyx data instead of snapshots */
143+
shouldUseLiveData: boolean;
142144
setCurrentSearchHashAndKey: (hash: number, key: SearchKey | undefined) => void;
143145
setCurrentSearchQueryJSON: (searchQueryJSON: SearchQueryJSON | undefined) => void;
144146
/** If you want to set `selectedTransactionIDs`, pass an array as the first argument, object/record otherwise */

src/hooks/useOnyx.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,18 @@ const useOnyx: OriginalUseOnyx = <TKey extends OnyxKey, TReturnValue = OnyxValue
5252
const isOnSearch = useIsOnSearch();
5353

5454
let currentSearchHash: number | undefined;
55+
let shouldUseLiveData = false;
5556
if (isOnSearch && isSnapshotCompatibleKey) {
56-
const {currentSearchHash: searchContextCurrentSearchHash} = use(SearchContext);
57+
const {currentSearchHash: searchContextCurrentSearchHash, shouldUseLiveData: contextShouldUseLiveData} = use(SearchContext);
5758
currentSearchHash = searchContextCurrentSearchHash;
59+
shouldUseLiveData = !!contextShouldUseLiveData;
5860
}
5961

6062
const useOnyxOptions = options as UseOnyxOptions<OnyxKey, OnyxValue<OnyxKey>> | undefined;
6163
const {selector: selectorProp, ...optionsWithoutSelector} = useOnyxOptions ?? {};
6264

6365
// Determine if we should use snapshot data based on search state and key
64-
const shouldUseSnapshot = isOnSearch && !!currentSearchHash && isSnapshotCompatibleKey;
66+
const shouldUseSnapshot = isOnSearch && !!currentSearchHash && isSnapshotCompatibleKey && !shouldUseLiveData;
6567

6668
// Create selector function that handles both regular and snapshot data
6769
const selector = useMemo(() => {

src/hooks/useSearchHighlightAndScroll.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type UseSearchHighlightAndScroll = {
2323
searchKey: SearchKey | undefined;
2424
offset: number;
2525
shouldCalculateTotals: boolean;
26+
shouldUseLiveData: boolean;
2627
};
2728

2829
/**
@@ -38,6 +39,7 @@ function useSearchHighlightAndScroll({
3839
searchKey,
3940
offset,
4041
shouldCalculateTotals,
42+
shouldUseLiveData,
4143
}: UseSearchHighlightAndScroll) {
4244
const isFocused = useIsFocused();
4345
const {isOffline} = useNetwork();
@@ -147,12 +149,14 @@ function useSearchHighlightAndScroll({
147149
]);
148150

149151
useEffect(() => {
152+
// For live data, isLoading is always false, so we also need to reset when searchResultsData changes
153+
// For snapshot data, we wait for isLoading to become false after the API call completes
150154
if (searchResults?.search?.isLoading) {
151155
return;
152156
}
153157

154158
searchTriggeredRef.current = false;
155-
}, [searchResults?.search?.isLoading]);
159+
}, [searchResults?.search?.isLoading, shouldUseLiveData, searchResultsData]);
156160

157161
// Initialize the set with existing IDs only once
158162
useEffect(() => {

0 commit comments

Comments
 (0)