Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/components/Search/SearchResultsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ function SearchResultsProvider({children}: SearchResultsProviderProps) {
const hasResults = Object.keys(liveData.data).length > 0;
// For to-do searches, always return a valid SearchResults object (even with empty data)
// This ensures we show the empty state instead of loading/blocking views
// don't force isLoading off, the load-more skeleton reads it to tell a page is in flight
currentSearchResults = {
search: {...searchInfo, isLoading: false, hasResults},
search: {...searchInfo, hasResults},
data: liveData.data,
};
} else {
Expand Down
9 changes: 7 additions & 2 deletions src/components/Search/hooks/useSearchSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ type UseSearchSnapshotParams = {
* full-collection reads. */
transactions: OptimisticTrackingParams['transactions'];
reportActions: OptimisticTrackingParams['reportActions'];
/** Row cap for `data`. Live searches page on this instead of a server cursor. `undefined` = no cap. */
visibleRowLimit?: number;
};

const EMPTY_DATA: SearchListItem[] = [];
Expand All @@ -93,7 +95,7 @@ const hashToString = (queryHash?: number) => (queryHash || queryHash === 0 ? Str
* per-group sub-snapshots, and absorbs the optimistic-row resilience. Returns the sorted rows plus the
* list-level meta and the optimistic-tracking carriers that `<Search>` consumes.
*/
function useSearchSnapshot({queryJSON, searchResults, newSearchResultKeys, transactions, reportActions}: UseSearchSnapshotParams): SearchSnapshotResult {
function useSearchSnapshot({queryJSON, searchResults, newSearchResultKeys, transactions, reportActions, visibleRowLimit}: UseSearchSnapshotParams): SearchSnapshotResult {
const {type, sortBy, sortOrder, hash, groupBy} = queryJSON;

const {isOffline} = useNetwork();
Expand Down Expand Up @@ -401,8 +403,11 @@ function useSearchSnapshot({queryJSON, searchResults, newSearchResultKeys, trans
return item.transactions.length === 0 || !subSnapshot || !subSnapshot?.search?.hasMoreResults;
});

// slice after the sort so page 2 continues the order on screen
const visibleData = visibleRowLimit !== undefined && stableSortedData.length > visibleRowLimit ? stableSortedData.slice(0, visibleRowLimit) : stableSortedData;

return {
data: stableSortedData,
data: visibleData,
chartData,
filteredData,
filteredDataLength,
Expand Down
8 changes: 8 additions & 0 deletions src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,13 @@ function Search({

const [, cardFeedsResult] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER);

// one page of local rows per resolved snapshot offset. offset is written optimistically at request time,
// so hold the limit there until isLoading clears, otherwise the next rows show before the response lands
const liveRowLimit = Math.max(
CONST.SEARCH.RESULTS_PAGE_SIZE,
searchResults?.search?.isLoading ? (searchResults?.search?.offset ?? 0) : (searchResults?.search?.offset ?? 0) + CONST.SEARCH.RESULTS_PAGE_SIZE,
);

const searchDataType = useMemo(() => (shouldUseLiveData ? CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT : searchResults?.search?.type), [shouldUseLiveData, searchResults?.search?.type]);
const isExpenseAllMatchingSelection = type === CONST.SEARCH.DATA_TYPES.EXPENSE && areAllMatchingItemsSelected;
const isAllMatchingItemsCountMissing = isExpenseAllMatchingSelection && typeof searchResults?.search?.count !== 'number';
Expand Down Expand Up @@ -282,6 +289,7 @@ function Search({
newSearchResultKeys,
transactions,
reportActions,
visibleRowLimit: shouldUseLiveData ? liveRowLimit : undefined,
});

// Mirror `hasQueuedHighlights` into a ref so the post-create-flow `useFocusEffect`
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/Search/useSearchSnapshotTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,4 +429,36 @@ describe('useSearchSnapshot', () => {
expect(result.current.chartData).toBe(firstChartData);
expect(result.current.data).toBe(firstData);
});

it('caps data at visibleRowLimit while leaving chartData and the row count untouched', () => {
const searchResults = makeSearchResults();
mockUseOptimisticSearchTracking.mockReturnValue(trackingReturn(searchResults.data));
const rows = Array.from({length: 5}, (_value, index) => ({transactionID: `${index}`, keyForList: `${index}`}));
mockGetSections.mockReturnValue([rows, rows.length, false]);
mockGetSortedSections.mockReturnValue(rows);

const {result, rerender} = renderHook((visibleRowLimit?: number) =>
useSearchSnapshot({
queryJSON: makeQueryJSON(),
searchResults,
newSearchResultKeys: undefined,
transactions: undefined,
reportActions: undefined,
visibleRowLimit,
}),
);

expect(result.current.data).toHaveLength(5);

rerender(2);

expect(result.current.data.map((item) => item.keyForList)).toEqual(['0', '1']);
expect(result.current.chartData).toHaveLength(5);
expect(result.current.filteredDataLength).toBe(5);

rerender(10);

// a fresh array here re-renders the list on every pass
expect(result.current.data).toBe(result.current.chartData);
});
});
Loading