Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ function MoneyReportHeader({
> | null>(null);

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

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

Expand Down
26 changes: 25 additions & 1 deletion src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ function Search({
const suggestedSearches = useMemo(() => getSuggestedSearches(accountID, defaultCardFeed?.id), [defaultCardFeed?.id, accountID]);
const searchKey = useMemo(() => Object.values(suggestedSearches).find((search) => search.similarSearchHash === similarSearchHash)?.key, [suggestedSearches, similarSearchHash]);
const searchDataType = useMemo(() => (shouldUseLiveData ? CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT : searchResults?.search?.type), [shouldUseLiveData, searchResults?.search?.type]);
const shouldCalculateTotals = useSearchShouldCalculateTotals(searchKey, similarSearchHash, offset === 0);
const shouldCalculateTotals = useSearchShouldCalculateTotals(searchKey, hash, offset === 0);

const previousReportActions = usePrevious(reportActions);
const {translate, localeCompare, formatPhoneNumber} = useLocalize();
Expand Down Expand Up @@ -545,6 +545,8 @@ function Search({
setShouldShowFiltersBarLoading(shouldShowLoadingState && lastSearchType !== type);
}, [lastSearchType, setShouldShowFiltersBarLoading, shouldShowLoadingState, type]);

const shouldRetrySearchWithTotalsRef = useRef(false);

useEffect(() => {
const focusedRoute = findFocusedRoute(navigationRef.getRootState());
const isMigratedModalDisplayed = focusedRoute?.name === NAVIGATORS.MIGRATED_USER_MODAL_NAVIGATOR || focusedRoute?.name === SCREENS.MIGRATED_USER_WELCOME_MODAL.ROOT;
Expand All @@ -554,12 +556,34 @@ function Search({
return;
}

if (searchResults?.search?.isLoading) {
if (shouldCalculateTotals && searchResults?.search?.count === undefined) {
shouldRetrySearchWithTotalsRef.current = true;
}
return;
}

handleSearch({queryJSON, searchKey, offset, shouldCalculateTotals, prevReportsLength: filteredDataLength, isLoading: !!searchResults?.search?.isLoading});

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

useEffect(() => {
if (!shouldRetrySearchWithTotalsRef.current || searchResults?.search?.isLoading || !shouldCalculateTotals) {
return;
}

// If count is already present, the latest response already contains totals and we can skip the re-query.
if (searchResults?.search?.count !== undefined) {
shouldRetrySearchWithTotalsRef.current = false;
return;
}

shouldRetrySearchWithTotalsRef.current = false;
handleSearch({queryJSON, searchKey, offset, shouldCalculateTotals: true, prevReportsLength: filteredDataLength, isLoading: false});
}, [filteredDataLength, handleSearch, offset, queryJSON, searchKey, searchResults?.search?.count, searchResults?.search?.isLoading, shouldCalculateTotals]);

// When new data load, selectedTransactions is updated in next effect. We use this flag to whether selection is updated
const isRefreshingSelection = useRef(false);

Expand Down
25 changes: 7 additions & 18 deletions src/hooks/useSearchShouldCalculateTotals.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import {useMemo} from 'react';
import {buildSearchQueryJSON} from '@libs/SearchQueryUtils';
import type {SearchKey} from '@libs/SearchUIUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import useOnyx from './useOnyx';

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

const shouldCalculateTotals = useMemo(() => {
if (!enabled) {
return false;
}

const savedSearchValues = Object.values(savedSearches ?? {});

if (!savedSearchValues.length && !searchKey) {
if (!Object.keys(savedSearches ?? {}).length && !searchKey) {
return false;
}

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

if (eligibleSearchKeys.includes(searchKey)) {
return true;
}

for (const savedSearch of savedSearchValues) {
const searchData = buildSearchQueryJSON(savedSearch.query);
if (searchData && searchData.similarSearchHash === similarSearchHash) {
return true;
}
}
const isSuggestedSearchWithTotals = eligibleSearchKeys.includes(searchKey);
const isSavedSearch = searchHash !== undefined && savedSearches && !!savedSearches[searchHash];

return false;
}, [enabled, savedSearches, searchKey, similarSearchHash]);
return isSuggestedSearchWithTotals || isSavedSearch;
}, [enabled, savedSearches, searchKey, searchHash]);

return shouldCalculateTotals;
return shouldCalculateTotals ?? false;
}

export default useSearchShouldCalculateTotals;
26 changes: 21 additions & 5 deletions src/pages/Search/SearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
import usePrevious from '@hooks/usePrevious';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSearchShouldCalculateTotals from '@hooks/useSearchShouldCalculateTotals';
import useSelfDMReport from '@hooks/useSelfDMReport';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
Expand Down Expand Up @@ -109,8 +110,17 @@ function SearchPage({route}: SearchPageProps) {
const theme = useTheme();
const {isOffline} = useNetwork();
const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext);
const {selectedTransactions, clearSelectedTransactions, selectedReports, lastSearchType, setLastSearchType, areAllMatchingItemsSelected, selectAllMatchingItems, currentSearchResults} =
useSearchContext();
const {
selectedTransactions,
clearSelectedTransactions,
selectedReports,
lastSearchType,
setLastSearchType,
areAllMatchingItemsSelected,
selectAllMatchingItems,
currentSearchKey,
currentSearchResults,
} = useSearchContext();
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const isMobileSelectionModeEnabled = useMobileSelectionMode(clearSelectedTransactions);
const allTransactions = useAllTransactions();
Expand Down Expand Up @@ -1175,7 +1185,8 @@ function SearchPage({route}: SearchPageProps) {
const {resetVideoPlayerData} = usePlaybackContext();

const metadata = searchResults?.search;
const shouldShowFooter = !!metadata?.count || selectedTransactionsKeys.length > 0;
const shouldAllowFooterTotals = useSearchShouldCalculateTotals(currentSearchKey, queryJSON?.hash, true);
const shouldShowFooter = selectedTransactionsKeys.length > 0 || (shouldAllowFooterTotals && !!metadata?.count);

// Handles video player cleanup:
// 1. On mount: Resets player if navigating from report screen
Expand Down Expand Up @@ -1217,7 +1228,11 @@ function SearchPage({route}: SearchPageProps) {
}, []);

const footerData = useMemo(() => {
const shouldUseClientTotal = !metadata?.count || (selectedTransactionsKeys.length > 0 && !areAllMatchingItemsSelected);
if (!shouldAllowFooterTotals && selectedTransactionsKeys.length === 0) {
return {count: undefined, total: undefined, currency: undefined};
}

const shouldUseClientTotal = selectedTransactionsKeys.length > 0 || !metadata?.count || (selectedTransactionsKeys.length > 0 && !areAllMatchingItemsSelected);
const selectedTransactionItems = Object.values(selectedTransactions);
const currency = metadata?.currency ?? selectedTransactionItems.at(0)?.groupCurrency;
const numberOfExpense = shouldUseClientTotal
Expand All @@ -1233,7 +1248,7 @@ function SearchPage({route}: SearchPageProps) {
const total = shouldUseClientTotal ? selectedTransactionItems.reduce((acc, transaction) => acc - (transaction.groupAmount ?? 0), 0) : metadata?.total;

return {count: numberOfExpense, total, currency};
}, [areAllMatchingItemsSelected, metadata?.count, metadata?.currency, metadata?.total, selectedTransactions, selectedTransactionsKeys]);
}, [areAllMatchingItemsSelected, metadata?.count, metadata?.currency, metadata?.total, selectedTransactions, selectedTransactionsKeys, shouldAllowFooterTotals]);

const onSortPressedCallback = useCallback(() => {
setIsSorting(true);
Expand Down Expand Up @@ -1296,6 +1311,7 @@ function SearchPage({route}: SearchPageProps) {
currentSelectedReportID={selectedTransactionReportIDs?.at(0) ?? selectedReportIDs?.at(0)}
confirmPayment={onBulkPaySelected}
latestBankItems={latestBankItems}
shouldShowFooter={shouldShowFooter}
/>
<DragAndDropConsumer onDrop={initScanRequest}>
<DropZoneUI
Expand Down
5 changes: 3 additions & 2 deletions src/pages/Search/SearchPageNarrow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ type SearchPageNarrowProps = {
currentSelectedReportID?: string | undefined;
confirmPayment?: (paymentType: PaymentMethodType | undefined) => void;
latestBankItems?: BankAccountMenuItem[] | undefined;
shouldShowFooter: boolean;
};

function SearchPageNarrow({
Expand All @@ -71,13 +72,14 @@ function SearchPageNarrow({
currentSelectedReportID,
latestBankItems,
confirmPayment,
shouldShowFooter,
}: SearchPageNarrowProps) {
const {translate} = useLocalize();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {windowHeight} = useWindowDimensions();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {clearSelectedTransactions, selectedTransactions} = useSearchContext();
const {clearSelectedTransactions} = useSearchContext();
const [searchRouterListVisible, setSearchRouterListVisible] = useState(false);
const {isOffline} = useNetwork();
// Controls the visibility of the educational tooltip based on user scrolling.
Expand Down Expand Up @@ -176,7 +178,6 @@ function SearchPageNarrow({
);
}

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

Expand Down
78 changes: 78 additions & 0 deletions tests/unit/hooks/useSearchShouldCalculateTotals.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {renderHook} from '@testing-library/react-native';
import useSearchShouldCalculateTotals from '@hooks/useSearchShouldCalculateTotals';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';

const onyxData: Record<string, unknown> = {};

const mockUseOnyx = jest.fn(
(
key: string,
options?: {
selector?: (value: unknown) => unknown;
},
) => {
const value = onyxData[key];
const selectedValue = options?.selector ? options.selector(value as never) : value;
return [selectedValue];
},
);

jest.mock('@hooks/useOnyx', () => ({
// eslint-disable-next-line @typescript-eslint/naming-convention
__esModule: true,
default: (key: string, options?: {selector?: (value: unknown) => unknown}) => mockUseOnyx(key, options),
}));

describe('useSearchShouldCalculateTotals', () => {
beforeEach(() => {
onyxData[ONYXKEYS.SAVED_SEARCHES] = undefined;
mockUseOnyx.mockClear();
});

it('returns false when disabled', () => {
const {result} = renderHook(() => useSearchShouldCalculateTotals(CONST.SEARCH.SEARCH_KEYS.SUBMIT, 123, false));

expect(result.current).toBe(false);
});

it('returns true for eligible suggested searches', () => {
const {result} = renderHook(() => useSearchShouldCalculateTotals(CONST.SEARCH.SEARCH_KEYS.SUBMIT, 123, true));

expect(result.current).toBe(true);
});

it('returns false for non-eligible searches', () => {
const {result} = renderHook(() => useSearchShouldCalculateTotals(CONST.SEARCH.SEARCH_KEYS.EXPENSES, 123, true));

expect(result.current).toBe(false);
});

it('returns true for saved searches that match the hash', () => {
onyxData[ONYXKEYS.SAVED_SEARCHES] = {
// eslint-disable-next-line @typescript-eslint/naming-convention
456: {
name: 'My search',
query: 'type:expense',
},
};

const {result} = renderHook(() => useSearchShouldCalculateTotals(undefined, 456, true));

expect(result.current).toBe(true);
});

it('returns false when saved searches do not match the hash', () => {
onyxData[ONYXKEYS.SAVED_SEARCHES] = {
// eslint-disable-next-line @typescript-eslint/naming-convention
456: {
name: 'My search',
query: 'type:expense',
},
};

const {result} = renderHook(() => useSearchShouldCalculateTotals(undefined, 789, true));

expect(result.current).toBe(false);
});
});
Loading