diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 759affeb3c0d3..344b4fe8e8710 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -580,6 +580,8 @@ const ONYXKEYS = {
/** List of transaction thread IDs used when navigating to prev/next transaction when viewing it in RHP */
TRANSACTION_THREAD_NAVIGATION_REPORT_IDS: 'transactionThreadNavigationReportIDs',
+ REPORT_NAVIGATION_LAST_SEARCH_QUERY: 'ReportNavigationLastSearchQuery',
+
/** Timestamp of the last login on iOS */
NVP_LAST_ECASH_IOS_LOGIN: 'nvp_lastECashIOSLogin',
NVP_LAST_IPHONE_LOGIN: 'nvp_lastiPhoneLogin',
@@ -1267,6 +1269,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_LAST_ECASH_IOS_LOGIN]: string;
[ONYXKEYS.NVP_LAST_ECASH_ANDROID_LOGIN]: string;
[ONYXKEYS.NVP_LAST_IPHONE_LOGIN]: string;
+ [ONYXKEYS.REPORT_NAVIGATION_LAST_SEARCH_QUERY]: OnyxTypes.LastSearchParams;
[ONYXKEYS.NVP_LAST_ANDROID_LOGIN]: string;
[ONYXKEYS.TRANSACTION_THREAD_NAVIGATION_REPORT_IDS]: string[];
[ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES]: OnyxTypes.ExportTemplate[];
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index aa2eb9cbb8d52..74dff42e47155 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -130,6 +130,7 @@ import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar';
import MoneyReportHeaderStatusBarSkeleton from './MoneyReportHeaderStatusBarSkeleton';
import type {MoneyRequestHeaderStatusBarProps} from './MoneyRequestHeaderStatusBar';
import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar';
+import MoneyRequestReportNavigation from './MoneyRequestReportView/MoneyRequestReportNavigation';
import type {PopoverMenuItem} from './PopoverMenu';
import type {ActionHandledType} from './ProcessMoneyReportHoldMenu';
import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu';
@@ -401,6 +402,8 @@ function MoneyReportHeader({
const isReportInRHP = route.name === SCREENS.SEARCH.REPORT_RHP;
const shouldDisplaySearchRouter = !isReportInRHP || isSmallScreenWidth;
+ const isReportInSearch = route.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT;
+
const existingB2BInvoiceReport = useParticipantsInvoiceReport(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport?.policyID);
const confirmPayment = useCallback(
(type?: PaymentMethodType | undefined, payAsBusiness?: boolean, methodID?: number, paymentMethod?: PaymentMethod) => {
@@ -456,14 +459,6 @@ function MoneyReportHeader({
} else {
startApprovedAnimation();
approveMoneyRequest(moneyRequestReport, true);
- if (currentSearchQueryJSON) {
- search({
- searchKey: currentSearchKey,
- shouldCalculateTotals: true,
- offset: 0,
- queryJSON: currentSearchQueryJSON,
- });
- }
}
};
@@ -1251,44 +1246,6 @@ function MoneyReportHeader({
);
- const hasOtherItems =
- (shouldShowNextStep && !!optimisticNextStep?.message?.length) || (shouldShowNextStep && !optimisticNextStep && !!isLoadingInitialReportActions && !isOffline) || !!statusBarProps;
-
- const moreContentUnfiltered = [
- shouldShowSelectedTransactionsButton && shouldDisplayNarrowVersion && (
-
- null}
- options={selectedTransactionsOptions}
- customText={translate('workspace.common.selected', {count: selectedTransactionIDs.length})}
- isSplitButton={false}
- shouldAlwaysShowDropdownMenu
- wrapperStyle={styles.w100}
- />
-
- ),
- shouldShowNextStep && !!optimisticNextStep?.message?.length && (
-
- ),
- shouldShowNextStep && !optimisticNextStep && !!isLoadingInitialReportActions && !isOffline && ,
- !!statusBarProps && (
-
- ),
- ];
- const moreContent = moreContentUnfiltered.filter(Boolean);
- const isMoreContentShown = moreContent.length > 0;
- const shouldAddGapToContents = moreContent.length > 1;
-
return (
)}
- {shouldDisplayNarrowVersion && !shouldShowSelectedTransactionsButton && (
-
- {!!primaryAction && {primaryActionsImplementation[primaryAction]}}
- {!!applicableSecondaryActions.length && KYCMoreDropdown}
-
- )}
- {isMoreContentShown && {moreContent}}
+ {shouldDisplayNarrowVersion &&
+ (shouldShowSelectedTransactionsButton ? (
+
+ null}
+ options={selectedTransactionsOptions}
+ customText={translate('workspace.common.selected', {count: selectedTransactionIDs.length})}
+ isSplitButton={false}
+ shouldAlwaysShowDropdownMenu
+ wrapperStyle={styles.w100}
+ />
+
+ ) : (
+
+ {!!primaryAction && {primaryActionsImplementation[primaryAction]}}
+ {!!applicableSecondaryActions.length && KYCMoreDropdown}
+
+ ))}
+
+
+
+ {shouldShowNextStep && !!optimisticNextStep?.message?.length && }
+ {shouldShowNextStep && !optimisticNextStep && !!isLoadingInitialReportActions && !isOffline && }
+ {!!statusBarProps && (
+
+ )}
+
+ {isReportInSearch && (
+
+ )}
+
{isHoldMenuVisible && requestType !== undefined && (
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx
new file mode 100644
index 0000000000000..06bb6b6911e97
--- /dev/null
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx
@@ -0,0 +1,135 @@
+import React, {useEffect} from 'react';
+import {View} from 'react-native';
+import PrevNextButtons from '@components/PrevNextButtons';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {selectArchivedReportsIdSet, selectFilteredReportActions} from '@libs/ReportUtils';
+import {getSections, getSortedSections} from '@libs/SearchUIUtils';
+import Navigation from '@navigation/Navigation';
+import {saveLastSearchParams} from '@userActions/ReportNavigation';
+import {search} from '@userActions/Search';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+
+type MoneyRequestReportNavigationProps = {
+ reportID?: string;
+ shouldDisplayNarrowVersion: boolean;
+};
+
+function MoneyRequestReportNavigation({reportID, shouldDisplayNarrowVersion}: MoneyRequestReportNavigationProps) {
+ const [lastSearchQuery] = useOnyx(ONYXKEYS.REPORT_NAVIGATION_LAST_SEARCH_QUERY, {canBeMissing: true});
+ const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${lastSearchQuery?.queryJSON?.hash}`, {canBeMissing: true});
+ const [accountID] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: (s) => s?.accountID});
+
+ const {localeCompare, formatPhoneNumber} = useLocalize();
+
+ const [exportReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {
+ canEvict: false,
+ canBeMissing: true,
+ selector: selectFilteredReportActions,
+ });
+
+ const [archivedReportsIdSet = new Set()] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {
+ canBeMissing: true,
+ selector: selectArchivedReportsIdSet,
+ });
+
+ const {type, status, sortBy, sortOrder, groupBy} = lastSearchQuery?.queryJSON ?? {};
+ let results: Array = [];
+ if (!!type && !!groupBy && !!currentSearchResults?.data && !!currentSearchResults?.search) {
+ const searchData = getSections(type, currentSearchResults.data, accountID, formatPhoneNumber, groupBy, exportReportActions, lastSearchQuery?.searchKey, archivedReportsIdSet);
+ results = getSortedSections(type, status ?? '', searchData, localeCompare, sortBy, sortOrder, groupBy).map((value) => value.reportID);
+ }
+ const allReports = results;
+
+ const currentIndex = allReports.indexOf(reportID);
+ const allReportsCount = lastSearchQuery?.previousLengthOfResults ?? 0;
+
+ const hideNextButton = !lastSearchQuery?.hasMoreResults && currentIndex === allReports.length - 1;
+ const hidePrevButton = currentIndex === 0;
+ const styles = useThemeStyles();
+ const shouldDisplayNavigationArrows = allReports && allReports.length > 1 && currentIndex !== -1 && !!lastSearchQuery?.queryJSON;
+
+ useEffect(() => {
+ if (!lastSearchQuery?.queryJSON) {
+ return;
+ }
+
+ if (lastSearchQuery.allowPostSearchRecount) {
+ saveLastSearchParams({
+ ...lastSearchQuery,
+ allowPostSearchRecount: false,
+ previousLengthOfResults: allReports.length,
+ });
+ return;
+ }
+
+ if (currentIndex < allReportsCount - 1) {
+ return;
+ }
+
+ saveLastSearchParams({
+ ...lastSearchQuery,
+ previousLengthOfResults: allReports.length,
+ });
+ }, [currentIndex, allReportsCount, allReports.length, lastSearchQuery?.queryJSON, lastSearchQuery]);
+
+ const goToReportId = (reportId?: string) => {
+ if (!reportId) {
+ return;
+ }
+ Navigation.setParams({
+ reportID: reportId,
+ });
+ };
+
+ const goToNextReport = () => {
+ if (currentIndex === -1 || allReports.length === 0 || !lastSearchQuery?.queryJSON) {
+ return;
+ }
+ const threshold = Math.min(allReports.length * 0.75, allReports.length - 2);
+
+ if (currentIndex + 1 >= threshold && lastSearchQuery?.hasMoreResults) {
+ const newOffset = (lastSearchQuery.offset ?? 0) + CONST.SEARCH.RESULTS_PAGE_SIZE;
+ search({
+ queryJSON: lastSearchQuery.queryJSON,
+ offset: newOffset,
+ prevReportsLength: allReports.length,
+ shouldCalculateTotals: false,
+ searchKey: lastSearchQuery.searchKey,
+ });
+ }
+
+ const nextIndex = (currentIndex + 1) % allReports.length;
+ goToReportId(allReports.at(nextIndex));
+ };
+
+ const goToPrevReport = () => {
+ if (currentIndex === -1 || allReports.length === 0) {
+ return;
+ }
+
+ const prevIndex = (currentIndex - 1) % allReports.length;
+ goToReportId(allReports.at(prevIndex));
+ };
+
+ return (
+ shouldDisplayNavigationArrows && (
+
+ {!shouldDisplayNarrowVersion && {`${currentIndex + 1} of ${allReportsCount}`}}
+
+
+ )
+ );
+}
+
+MoneyRequestReportNavigation.displayName = 'MoneyRequestReportNavigation';
+
+export default MoneyRequestReportNavigation;
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx
index 8ea11ad41bf9f..4946ee1f3991b 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx
@@ -47,6 +47,9 @@ type MoneyRequestReportViewProps = {
/** Whether Report footer (that includes Composer) should be displayed */
shouldDisplayReportFooter: boolean;
+ /** Whether we should wait for the report to sync */
+ shouldWaitForReportSync: boolean;
+
/** The `backTo` route that should be used when clicking back button */
backToRoute: Route | undefined;
};
@@ -86,7 +89,7 @@ function getParentReportAction(parentReportActions: OnyxEntry reportActions.some((action) => isSentMoneyReportAction(action)), [reportActions]);
- const newTransactions = useNewTransactions(reportMetadata?.hasOnceLoadedReportActions, transactions);
-
+ const newTransactions = useNewTransactions(reportMetadata?.hasOnceLoadedReportActions, shouldWaitForReportSync ? [] : transactions);
const [parentReportAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(report?.parentReportID)}`, {
canEvict: false,
canBeMissing: true,
@@ -172,7 +174,7 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe
[backToRoute, isLoadingInitialReportActions, isTransactionThreadView, parentReportAction, policy, report, reportActions, transactionThreadReportID],
);
- if (!!(isLoadingInitialReportActions && reportActions.length === 0 && !isOffline) || shouldWaitForTransactions) {
+ if (!!(isLoadingInitialReportActions && reportActions.length === 0 && !isOffline) || shouldWaitForTransactions || shouldWaitForReportSync) {
return ;
}
diff --git a/src/components/PrevNextButtons.tsx b/src/components/PrevNextButtons.tsx
index 23f950d30f524..cd92b59f14ede 100644
--- a/src/components/PrevNextButtons.tsx
+++ b/src/components/PrevNextButtons.tsx
@@ -33,7 +33,7 @@ function PrevNextButtons({isPrevButtonDisabled, isNextButtonDisabled, onNext, on
accessibilityRole={CONST.ROLE.BUTTON}
accessibilityLabel={CONST.ROLE.BUTTON}
disabled={isPrevButtonDisabled}
- style={[styles.h10, styles.mr1, styles.alignItemsCenter, styles.justifyContentCenter]}
+ style={[styles.h7, styles.mr1, styles.alignItemsCenter, styles.justifyContentCenter]}
onPress={onPrevious}
>
@@ -50,7 +50,7 @@ function PrevNextButtons({isPrevButtonDisabled, isNextButtonDisabled, onNext, on
accessibilityRole={CONST.ROLE.BUTTON}
accessibilityLabel={CONST.ROLE.BUTTON}
disabled={isNextButtonDisabled}
- style={[styles.h10, styles.alignItemsCenter, styles.justifyContentCenter]}
+ style={[styles.h7, styles.alignItemsCenter, styles.justifyContentCenter]}
onPress={onNext}
>
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index 52066eeb44af0..58f80dbc8a7ee 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -3,7 +3,6 @@ import {accountIDSelector} from '@selectors/Session';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import type {NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
-import type {OnyxCollection} from 'react-native-onyx';
import Animated, {FadeIn, FadeOut, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import FullPageErrorView from '@components/BlockingViews/FullPageErrorView';
import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
@@ -26,9 +25,10 @@ import Log from '@libs/Log';
import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types';
import Performance from '@libs/Performance';
-import {getIOUActionForTransactionID, isExportIntegrationAction, isIntegrationMessageAction} from '@libs/ReportActionsUtils';
-import {canEditFieldOfMoneyRequest, isArchivedReport} from '@libs/ReportUtils';
+import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils';
+import {canEditFieldOfMoneyRequest, selectArchivedReportsIdSet, selectFilteredReportActions} from '@libs/ReportUtils';
import {buildCannedSearchQuery, buildSearchQueryJSON, buildSearchQueryString} from '@libs/SearchQueryUtils';
+import type {SearchKey} from '@libs/SearchUIUtils';
import {
createAndOpenSearchTransactionThread,
getColumnsToShow,
@@ -49,7 +49,6 @@ import {
shouldShowEmptyState,
shouldShowYear as shouldShowYearUtil,
} from '@libs/SearchUIUtils';
-import type {ArchivedReportsIDSet, SearchKey} from '@libs/SearchUIUtils';
import {isOnHold, isTransactionPendingDelete} from '@libs/TransactionUtils';
import Navigation, {navigationRef} from '@navigation/Navigation';
import type {SearchFullscreenNavigatorParamList} from '@navigation/types';
@@ -59,7 +58,7 @@ import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
-import type {OutstandingReportsByPolicyIDDerivedValue, ReportAction, ReportActions, ReportNameValuePairs} from '@src/types/onyx';
+import type {OutstandingReportsByPolicyIDDerivedValue, ReportAction} from '@src/types/onyx';
import type SearchResults from '@src/types/onyx/SearchResults';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import arraysEqual from '@src/utils/arraysEqual';
@@ -189,33 +188,6 @@ function prepareTransactionsList(
};
}
-const archivedReportsSelector = (all: OnyxCollection): ArchivedReportsIDSet => {
- const ids = new Set();
- if (!all) {
- return ids;
- }
-
- const prefixLength = ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS.length;
- for (const [key, value] of Object.entries(all)) {
- if (isArchivedReport(value)) {
- const reportID = key.slice(prefixLength);
- ids.add(reportID);
- }
- }
- return ids;
-};
-
-const exportReportActionsSelector = (allReportActions: OnyxCollection) => {
- return Object.fromEntries(
- Object.entries(allReportActions ?? {}).map(([reportID, reportActionsGroup]) => {
- const filteredReportActions = Object.values(reportActionsGroup ?? {}).filter(
- (action) => isExportIntegrationAction(action as ReportAction) || isIntegrationMessageAction(action as ReportAction),
- );
- return [reportID, filteredReportActions];
- }),
- );
-};
-
function Search({queryJSON, searchResults, onSearchListScroll, contentContainerStyle, handleSearch, isMobileSelectionModeEnabled, onSortPressedCallback}: SearchProps) {
const {isOffline} = useNetwork();
const {shouldUseNarrowLayout} = useResponsiveLayout();
@@ -251,13 +223,13 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
const [archivedReportsIdSet = new Set()] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {
canBeMissing: true,
- selector: archivedReportsSelector,
+ selector: selectArchivedReportsIdSet,
});
const [exportReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {
canEvict: false,
canBeMissing: true,
- selector: exportReportActionsSelector,
+ selector: selectFilteredReportActions,
});
const {defaultCardFeed} = useCardFeedsForDisplay();
@@ -365,20 +337,6 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSmallScreenWidth, selectedTransactions, isMobileSelectionModeEnabled]);
- useEffect(() => {
- const focusedRoute = findFocusedRoute(navigationRef.getRootState());
- const isMigratedModalDisplayed = focusedRoute?.name === NAVIGATORS.MIGRATED_USER_MODAL_NAVIGATOR || focusedRoute?.name === SCREENS.MIGRATED_USER_WELCOME_MODAL.ROOT;
-
- if ((!isFocused && !isMigratedModalDisplayed) || isOffline) {
- return;
- }
-
- handleSearch({queryJSON, searchKey, offset, shouldCalculateTotals});
- // We don't need to run the effect on change of isFocused.
- // eslint-disable-next-line react-compiler/react-compiler
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [handleSearch, isOffline, offset, queryJSON, searchKey, shouldCalculateTotals]);
-
useEffect(() => {
openSearch();
}, []);
@@ -403,19 +361,20 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
const shouldShowLoadingMoreItems = !shouldShowLoadingState && searchResults?.search?.isLoading && searchResults?.search?.offset > 0;
const prevIsSearchResultEmpty = usePrevious(isSearchResultsEmpty);
- const data = useMemo(() => {
+ const [data, dataLength] = useMemo(() => {
if (searchResults === undefined || !isDataLoaded) {
- return [];
+ return [[], 0];
}
// Group-by option cannot be used for chats or tasks
const isChat = type === CONST.SEARCH.DATA_TYPES.CHAT;
const isTask = type === CONST.SEARCH.DATA_TYPES.TASK;
if (groupBy && (isChat || isTask)) {
- return [];
+ return [[], 0];
}
- return getSections(type, searchResults.data, accountID, formatPhoneNumber, groupBy, exportReportActions, searchKey, archivedReportsIdSet, queryJSON);
+ const data1 = getSections(type, searchResults.data, accountID, formatPhoneNumber, groupBy, exportReportActions, searchKey, archivedReportsIdSet, queryJSON);
+ return [data1, data1.length];
}, [searchKey, exportReportActions, groupBy, isDataLoaded, searchResults, type, archivedReportsIdSet, formatPhoneNumber, accountID, queryJSON]);
useEffect(() => {
@@ -423,6 +382,21 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
setShouldShowFiltersBarLoading(shouldShowLoadingState && lastSearchType !== type);
}, [lastSearchType, setShouldShowFiltersBarLoading, shouldShowLoadingState, type]);
+ useEffect(() => {
+ const focusedRoute = findFocusedRoute(navigationRef.getRootState());
+ const isMigratedModalDisplayed = focusedRoute?.name === NAVIGATORS.MIGRATED_USER_MODAL_NAVIGATOR || focusedRoute?.name === SCREENS.MIGRATED_USER_WELCOME_MODAL.ROOT;
+
+ if ((!isFocused && !isMigratedModalDisplayed) || isOffline) {
+ return;
+ }
+
+ handleSearch({queryJSON, searchKey, offset, shouldCalculateTotals, prevReportsLength: dataLength});
+
+ // We don't need to run the effect on change of isFocused.
+ // eslint-disable-next-line react-compiler/react-compiler
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [handleSearch, isOffline, offset, queryJSON, searchKey, shouldCalculateTotals]);
+
// When new data load, selectedTransactions is updated in next effect. We use this flag to whether selection is updated
const isRefreshingSelection = useRef(false);
diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts
index 1d44fa7902862..e03fb612fa245 100644
--- a/src/components/Search/types.ts
+++ b/src/components/Search/types.ts
@@ -202,6 +202,7 @@ type SearchParams = {
queryJSON: SearchQueryJSON;
searchKey: SearchKey | undefined;
offset: number;
+ prevReportsLength?: number;
shouldCalculateTotals: boolean;
};
diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts
index ecec81a52df53..afe9be1b35f2d 100644
--- a/src/libs/API/index.ts
+++ b/src/libs/API/index.ts
@@ -311,4 +311,4 @@ function paginate {
+ const rootState = navigationRef.getRootState() as State | undefined;
+ if (!rootState) {
+ return false;
+ }
+
+ const lastRootRoute = rootState.routes.at(-1);
+ const lastNestedRoute = lastRootRoute?.state?.routes?.at(-1);
+ return lastNestedRoute?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT;
+};
+
+export default isOnSearchMoneyRequestReportPage;
diff --git a/src/libs/Navigation/helpers/isRHPOnSearchMoneyRequestReportPage.ts b/src/libs/Navigation/helpers/isRHPOnSearchMoneyRequestReportPage.ts
new file mode 100644
index 0000000000000..d91a3201c5c6d
--- /dev/null
+++ b/src/libs/Navigation/helpers/isRHPOnSearchMoneyRequestReportPage.ts
@@ -0,0 +1,16 @@
+import {navigationRef} from '@libs/Navigation/Navigation';
+import type {RootNavigatorParamList, State} from '@libs/Navigation/types';
+import SCREENS from '@src/SCREENS';
+
+const isRHPOnSearchMoneyRequestReportPage = (): boolean => {
+ const rootState = navigationRef.getRootState() as State | undefined;
+ if (!rootState) {
+ return false;
+ }
+
+ const lastRootRoute = rootState.routes.at(-2);
+ const lastNestedRoute = lastRootRoute?.state?.routes?.at(-1);
+ return lastNestedRoute?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT;
+};
+
+export default isRHPOnSearchMoneyRequestReportPage;
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 4b1a52cedb8fe..712a9d400ac96 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -226,6 +226,7 @@ import {
wasActionTakenByCurrentUser,
} from './ReportActionsUtils';
import type {LastVisibleMessage} from './ReportActionsUtils';
+import type {ArchivedReportsIDSet} from './SearchUIUtils';
import {getSession} from './SessionUtils';
import {shouldRestrictUserBillableActions} from './SubscriptionUtils';
import {
@@ -11576,6 +11577,39 @@ function getMoneyReportPreviewName(action: ReportAction, iouReport: OnyxEntry> | null | undefined): ArchivedReportsIDSet {
+ const archivedIDs = new Set();
+ if (!all) {
+ return archivedIDs;
+ }
+
+ const prefixLen = ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS.length;
+
+ for (const [key, value] of Object.entries(all)) {
+ if (isArchivedReport(value)) {
+ archivedIDs.add(key.slice(prefixLen));
+ }
+ }
+
+ return archivedIDs;
+}
+
+function selectFilteredReportActions(
+ reportActions: Record> | undefined> | null | undefined,
+): Record | undefined {
+ if (!reportActions) {
+ return {};
+ }
+
+ return Object.fromEntries(
+ Object.entries(reportActions).map(([reportId, actionsGroup]) => {
+ const actions = Object.values(actionsGroup ?? {});
+ const filteredActions = actions.filter((action): action is ReportAction => isExportIntegrationAction(action) || isIntegrationMessageAction(action));
+ return [reportId, filteredActions];
+ }),
+ );
+}
+
/**
* Returns the necessary reportAction onyx data to indicate that the transaction has been rejected optimistically
* @param [created] - Action created time
@@ -11998,6 +12032,8 @@ export {
parseReportRouteParams,
parseReportActionHtmlToText,
requiresAttentionFromCurrentUser,
+ selectArchivedReportsIdSet,
+ selectFilteredReportActions,
shouldAutoFocusOnKeyPress,
shouldCreateNewMoneyRequestReport,
shouldDisableDetailPage,
diff --git a/src/libs/actions/ReportNavigation.ts b/src/libs/actions/ReportNavigation.ts
new file mode 100644
index 0000000000000..682ff68f55dfe
--- /dev/null
+++ b/src/libs/actions/ReportNavigation.ts
@@ -0,0 +1,13 @@
+import Onyx from 'react-native-onyx';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type LastSearchParams from '@src/types/onyx/ReportNavigation';
+
+function saveLastSearchParams(value: LastSearchParams) {
+ Onyx.set(ONYXKEYS.REPORT_NAVIGATION_LAST_SEARCH_QUERY, value);
+}
+
+function clearLastSearchParams() {
+ Onyx.set(ONYXKEYS.REPORT_NAVIGATION_LAST_SEARCH_QUERY, {});
+}
+
+export {clearLastSearchParams, saveLastSearchParams};
diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts
index 65a07e6806d8b..8261832ae0edc 100644
--- a/src/libs/actions/Search.ts
+++ b/src/libs/actions/Search.ts
@@ -5,6 +5,7 @@ import type {FormOnyxValues} from '@components/Form/types';
import type {PaymentData, SearchQueryJSON} from '@components/Search/types';
import type {TransactionListItemType, TransactionReportGroupListItemType} from '@components/SelectionList/types';
import * as API from '@libs/API';
+import {waitForWrites} from '@libs/API';
import type {ExportSearchItemsToCSVParams, ExportSearchWithTemplateParams, ReportExportParams, SubmitReportParams} from '@libs/API/parameters';
import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import {getCommandURL} from '@libs/ApiUtils';
@@ -28,6 +29,15 @@ import type {PaymentInformation} from '@src/types/onyx/LastPaymentMethod';
import type {ConnectionName} from '@src/types/onyx/Policy';
import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults';
import type Nullable from '@src/types/utils/Nullable';
+import {saveLastSearchParams} from './ReportNavigation';
+
+type OnyxSearchResponse = {
+ data: [];
+ search: {
+ offset: number;
+ hasMoreResults: boolean;
+ };
+};
function handleActionButtonPress(
hash: number,
@@ -299,11 +309,13 @@ function search({
searchKey,
offset,
shouldCalculateTotals = false,
+ prevReportsLength,
}: {
queryJSON: SearchQueryJSON;
searchKey: SearchKey | undefined;
offset?: number;
shouldCalculateTotals?: boolean;
+ prevReportsLength?: number;
}) {
const {optimisticData, finallyData, failureData} = getOnyxLoadingData(queryJSON.hash, queryJSON, offset);
const {flatFilters, ...queryJSONWithoutFlatFilters} = queryJSON;
@@ -311,11 +323,47 @@ function search({
...queryJSONWithoutFlatFilters,
searchKey,
offset,
+ filters: queryJSONWithoutFlatFilters.filters ?? null,
shouldCalculateTotals,
};
const jsonQuery = JSON.stringify(query);
+ saveLastSearchParams({
+ queryJSON,
+ offset,
+ allowPostSearchRecount: false,
+ });
- API.read(READ_COMMANDS.SEARCH, {hash: queryJSON.hash, jsonQuery}, {optimisticData, finallyData, failureData});
+ waitForWrites(READ_COMMANDS.SEARCH).then(() => {
+ // eslint-disable-next-line rulesdir/no-api-side-effects-method
+ API.makeRequestWithSideEffects(READ_COMMANDS.SEARCH, {hash: queryJSON.hash, jsonQuery}, {optimisticData, finallyData, failureData}).then((result) => {
+ const response = result?.onyxData?.[0]?.value as OnyxSearchResponse;
+ const reports = Object.keys(response?.data ?? {})
+ .filter((key) => key.startsWith(ONYXKEYS.COLLECTION.REPORT))
+ .map((key) => key.replace(ONYXKEYS.COLLECTION.REPORT, ''));
+ if (response?.search?.offset) {
+ // Indicates that search results are extended from the Report view (with navigation between reports),
+ // using previous results to enable correct counter behavior.
+ if (prevReportsLength) {
+ saveLastSearchParams({
+ queryJSON,
+ offset,
+ hasMoreResults: !!response?.search?.hasMoreResults,
+ previousLengthOfResults: prevReportsLength,
+ allowPostSearchRecount: false,
+ });
+ }
+ } else {
+ // Applies to all searches from the Search View
+ saveLastSearchParams({
+ queryJSON,
+ offset,
+ hasMoreResults: !!response?.search?.hasMoreResults,
+ previousLengthOfResults: reports.length,
+ allowPostSearchRecount: true,
+ });
+ }
+ });
+ });
}
/**
diff --git a/src/pages/NewReportWorkspaceSelectionPage.tsx b/src/pages/NewReportWorkspaceSelectionPage.tsx
index 2a116f0625795..f31fc07b4304d 100644
--- a/src/pages/NewReportWorkspaceSelectionPage.tsx
+++ b/src/pages/NewReportWorkspaceSelectionPage.tsx
@@ -20,6 +20,7 @@ import {getHeaderMessageForNonUserList} from '@libs/OptionsListUtils';
import {isPolicyAdmin, shouldShowPolicy} from '@libs/PolicyUtils';
import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils';
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
+import isRHPOnSearchMoneyRequestReportPage from '@navigation/helpers/isRHPOnSearchMoneyRequestReportPage';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -37,29 +38,25 @@ function NewReportWorkspaceSelectionPage() {
const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
const {translate, localeCompare} = useLocalize();
const {shouldUseNarrowLayout} = useResponsiveLayout();
+ const isRHPOnReportInSearch = isRHPOnSearchMoneyRequestReportPage();
const [policies, fetchStatus] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true});
const shouldShowLoadingIndicator = isLoadingApp && !isOffline;
-
const navigateToNewReport = useCallback(
(optimisticReportID: string) => {
- if (shouldUseNarrowLayout) {
+ if (isRHPOnReportInSearch) {
Navigation.setNavigationActionToMicrotaskQueue(() => {
- Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: optimisticReportID}), {forceReplace: true});
+ Navigation.dismissModal();
});
- return;
}
- // On wide screens we use dismissModal instead of forceReplace to avoid performance issues
- Navigation.setNavigationActionToMicrotaskQueue(() => {
- Navigation.dismissModal();
- });
+
Navigation.setNavigationActionToMicrotaskQueue(() => {
- Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: optimisticReportID}));
+ Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: optimisticReportID}), {forceReplace: isRHPOnReportInSearch || shouldUseNarrowLayout});
});
},
- [shouldUseNarrowLayout],
+ [isRHPOnReportInSearch, shouldUseNarrowLayout],
);
const selectPolicy = useCallback(
diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx
index a916e8ad382fc..17ffd1d7e9aa2 100644
--- a/src/pages/Search/SearchMoneyRequestReportPage.tsx
+++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx
@@ -50,6 +50,8 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) {
const reportIDFromRoute = getNonEmptyStringOnyxID(route.params?.reportID);
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`, {allowStaleData: true, canBeMissing: true});
+ const shouldWaitForReportSync = report?.reportID !== reportIDFromRoute;
+
const [reportMetadata = defaultReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportIDFromRoute}`, {canBeMissing: true, allowStaleData: true});
const [policies = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {allowStaleData: true, canBeMissing: false});
const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`];
@@ -132,6 +134,8 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) {
reportMetadata={reportMetadata}
policy={policy}
shouldDisplayReportFooter={isCurrentReportLoadedFromOnyx}
+ shouldWaitForReportSync={shouldWaitForReportSync}
+ key={report?.reportID}
backToRoute={route.params.backTo}
/>
@@ -167,6 +171,8 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) {
reportMetadata={reportMetadata}
policy={policy}
shouldDisplayReportFooter={isCurrentReportLoadedFromOnyx}
+ shouldWaitForReportSync={shouldWaitForReportSync}
+ key={report?.reportID}
backToRoute={route.params.backTo}
/>
diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx
index df0571ca13593..8436284fbc4b1 100644
--- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx
+++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx
@@ -46,8 +46,10 @@ import {
import {getQuickActionIcon, getQuickActionTitle, isQuickActionAllowed} from '@libs/QuickActionUtils';
import {generateReportID, getDisplayNameForParticipant, getIcons, getReportName, getWorkspaceChats, isPolicyExpenseChat} from '@libs/ReportUtils';
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
+import isOnSearchMoneyRequestReportPage from '@navigation/helpers/isOnSearchMoneyRequestReportPage';
import variables from '@styles/variables';
import {closeReactNativeApp} from '@userActions/HybridApp';
+import {clearLastSearchParams} from '@userActions/ReportNavigation';
import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -149,6 +151,8 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref
selector: (policies) => Object.values(policies ?? {}).some((policy) => isPaidGroupPolicy(policy) && isPolicyMember(policy, currentUserPersonalDetails.login)),
});
+ const isReportInSearch = isOnSearchMoneyRequestReportPage();
+
const groupPoliciesWithChatEnabled = getGroupPaidPoliciesWithExpenseChatEnabled();
/**
@@ -480,6 +484,10 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref
workspaceIDForReportCreation = groupPoliciesWithChatEnabled.at(0)?.id;
}
+ if (isReportInSearch) {
+ clearLastSearchParams();
+ }
+
if (!workspaceIDForReportCreation || (shouldRestrictUserBillableActions(workspaceIDForReportCreation) && groupPoliciesWithChatEnabled.length > 1)) {
// If we couldn't guess the workspace to create the report, or a guessed workspace is past it's grace period and we have other workspaces to choose from
Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION);
@@ -493,6 +501,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref
isSearchTopmostFullScreenRoute()
? ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: createdReportID, backTo: Navigation.getActiveRoute()})
: ROUTES.REPORT_WITH_ID.getRoute(createdReportID, undefined, undefined, Navigation.getActiveRoute()),
+ {forceReplace: isReportInSearch},
);
});
} else {
diff --git a/src/styles/utils/flex.ts b/src/styles/utils/flex.ts
index aafda26f3acdd..82e41e7d37e14 100644
--- a/src/styles/utils/flex.ts
+++ b/src/styles/utils/flex.ts
@@ -113,6 +113,10 @@ export default {
flexWrap: 'wrap',
},
+ flexNoWrap: {
+ flexWrap: 'nowrap',
+ },
+
flexGrow0: {
flexGrow: 0,
},
diff --git a/src/styles/utils/sizing.ts b/src/styles/utils/sizing.ts
index cf1630fc1cab2..e1863a04a7dc9 100644
--- a/src/styles/utils/sizing.ts
+++ b/src/styles/utils/sizing.ts
@@ -13,6 +13,10 @@ export default {
height: '100%',
},
+ h7: {
+ height: 28,
+ },
+
h10: {
height: 40,
},
diff --git a/src/types/onyx/ReportNavigation.ts b/src/types/onyx/ReportNavigation.ts
new file mode 100644
index 0000000000000..a7af1edf38ba2
--- /dev/null
+++ b/src/types/onyx/ReportNavigation.ts
@@ -0,0 +1,38 @@
+import type {SearchQueryJSON} from '@components/Search/types';
+import type {SearchKey} from '@libs/SearchUIUtils';
+
+/**
+ * Represents the parameters from the previous search invocation.
+ * This is used to persist search arguments between navigations within reports,
+ * and allows loading more search results as the user continues navigating.
+ */
+type LastSearchParams = {
+ /**
+ * The number of results returned in the previous search.
+ */
+ previousLengthOfResults?: number;
+ /**
+ * Enables post-search recount based on extra criteria unknown during the initial search.
+ */
+ allowPostSearchRecount?: boolean;
+ /**
+ * Indicates whether there are more results available beyond the last search.
+ */
+ hasMoreResults?: boolean;
+
+ /**
+ * The full query JSON object that was used in the last search.
+ */
+ queryJSON?: SearchQueryJSON;
+ /**
+ * The current offset used in pagination for fetching the previous set of results.
+ */
+ offset?: number;
+
+ /**
+ *Optional witch field contain search key for last search performed
+ */
+ searchKey?: SearchKey;
+};
+
+export default LastSearchParams;
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index b1574fd787fb7..acc4d85e82d95 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -90,6 +90,7 @@ import type ReportActionsDraft from './ReportActionsDraft';
import type ReportActionsDrafts from './ReportActionsDrafts';
import type ReportMetadata from './ReportMetadata';
import type ReportNameValuePairs from './ReportNameValuePairs';
+import type LastSearchParams from './ReportNavigation';
import type ReportNextStep from './ReportNextStep';
import type ReportUserIsTyping from './ReportUserIsTyping';
import type {ReportFieldsViolations, ReportViolationName} from './ReportViolation';
@@ -271,6 +272,7 @@ export type {
SidePanel,
LastPaymentMethodType,
ReportAttributesDerivedValue,
+ LastSearchParams,
ReportTransactionsAndViolationsDerivedValue,
OutstandingReportsByPolicyIDDerivedValue,
ScheduleCallDraft,