diff --git a/src/components/ReportActionItem/MoneyRequestAction.tsx b/src/components/ReportActionItem/MoneyRequestAction.tsx index 447810524a6a..b9104a5a9668 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.tsx +++ b/src/components/ReportActionItem/MoneyRequestAction.tsx @@ -10,6 +10,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {createTransactionThreadReport} from '@libs/actions/Report'; +import getReportRouteForCurrentContext from '@libs/Navigation/helpers/getReportRouteForCurrentContext'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {TransactionDuplicateNavigatorParamList} from '@libs/Navigation/types'; @@ -131,7 +132,7 @@ function MoneyRequestAction({ Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: transactionThreadReport?.reportID, backTo: Navigation.getActiveRoute()})); return; } - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(transactionThreadReport?.reportID, undefined, undefined, Navigation.getActiveRoute())); + Navigation.navigate(getReportRouteForCurrentContext({reportID: transactionThreadReport?.reportID})); return; } @@ -140,7 +141,7 @@ function MoneyRequestAction({ return; } - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(action?.childReportID, undefined, undefined, Navigation.getActiveRoute())); + Navigation.navigate(getReportRouteForCurrentContext({reportID: action?.childReportID})); }; const isDeletedParentAction = isDeletedParentActionReportActionsUtils(action); diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index 4f9e9d559d14..ec9c18a39bb6 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -29,6 +29,7 @@ import ControlSelection from '@libs/ControlSelection'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import getButtonState from '@libs/getButtonState'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; +import getReportRouteForCurrentContext from '@libs/Navigation/helpers/getReportRouteForCurrentContext'; import Navigation from '@libs/Navigation/Navigation'; import Parser from '@libs/Parser'; import {getOriginalMessage} from '@libs/ReportActionsUtils'; @@ -36,7 +37,6 @@ import {isCanceledTaskReport, isOpenTaskReport, isReportManager} from '@libs/Rep import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; import type {Report, ReportAction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -143,7 +143,7 @@ function TaskPreview({ return ( Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(taskReportID, undefined, undefined, Navigation.getActiveRoute()))} + onPress={() => Navigation.navigate(getReportRouteForCurrentContext({reportID: taskReportID}))} onPressIn={() => canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} onLongPress={(event) => diff --git a/src/libs/Navigation/helpers/dismissModalForCurrentContext.ts b/src/libs/Navigation/helpers/dismissModalForCurrentContext.ts new file mode 100644 index 000000000000..13251710862b --- /dev/null +++ b/src/libs/Navigation/helpers/dismissModalForCurrentContext.ts @@ -0,0 +1,18 @@ +import Navigation from '@libs/Navigation/Navigation'; +import isSearchTopmostFullScreenRoute from './isSearchTopmostFullScreenRoute'; + +function dismissModalForCurrentContext(reportID?: string) { + if (isSearchTopmostFullScreenRoute()) { + Navigation.dismissModal(); + return; + } + + if (!reportID) { + Navigation.dismissModal(); + return; + } + + Navigation.dismissModalWithReport({reportID}); +} + +export default dismissModalForCurrentContext; diff --git a/src/libs/Navigation/helpers/getReportRouteForCurrentContext.ts b/src/libs/Navigation/helpers/getReportRouteForCurrentContext.ts new file mode 100644 index 000000000000..5b91b981db24 --- /dev/null +++ b/src/libs/Navigation/helpers/getReportRouteForCurrentContext.ts @@ -0,0 +1,22 @@ +import Navigation from '@libs/Navigation/Navigation'; +import ROUTES from '@src/ROUTES'; +import type {Route} from '@src/ROUTES'; +import isSearchTopmostFullScreenRoute from './isSearchTopmostFullScreenRoute'; + +type GetReportRouteForCurrentContextParams = { + reportID: string | undefined; + reportActionID?: string; + backTo?: Route; +}; + +function getReportRouteForCurrentContext({reportID, reportActionID, backTo}: GetReportRouteForCurrentContextParams): Route { + const currentRoute = backTo ?? (Navigation.getActiveRoute() as Route); + + if (isSearchTopmostFullScreenRoute()) { + return ROUTES.SEARCH_REPORT.getRoute({reportID, reportActionID, backTo: currentRoute}); + } + + return ROUTES.REPORT_WITH_ID.getRoute(reportID, reportActionID, undefined, currentRoute); +} + +export default getReportRouteForCurrentContext; diff --git a/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts b/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts index 00631ae00e5e..67c80ec5496c 100644 --- a/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts +++ b/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts @@ -1,11 +1,15 @@ -import {navigationRef} from '@libs/Navigation/Navigation'; +import navigationRef from '@libs/Navigation/navigationRef'; import type {RootNavigatorParamList, State} from '@libs/Navigation/types'; import NAVIGATORS from '@src/NAVIGATORS'; import getActiveTabName from './getActiveTabName'; import {isFullScreenName} from './isNavigatorName'; const isSearchTopmostFullScreenRoute = (): boolean => { - const rootState = navigationRef.getRootState() as State; + if (!navigationRef.isReady?.()) { + return false; + } + + const rootState = navigationRef.getRootState?.() as State; if (!rootState) { return false; diff --git a/src/libs/Navigation/helpers/shouldUseBackToOnLeaveReport.ts b/src/libs/Navigation/helpers/shouldUseBackToOnLeaveReport.ts new file mode 100644 index 000000000000..24a93a78905f --- /dev/null +++ b/src/libs/Navigation/helpers/shouldUseBackToOnLeaveReport.ts @@ -0,0 +1,72 @@ +import ROUTES from '@src/ROUTES'; +import type {Route} from '@src/ROUTES'; + +function normalizeRoute(route: string): string { + return route + .replaceAll(/\?.*/g, '') + .replaceAll(/^\/+|\/+$/g, '') + .replaceAll(/\/+/g, '/'); +} + +function getNestedBackToRoute(route: string): Route | undefined { + const [, queryString = ''] = route.split('?'); + + if (!queryString) { + return undefined; + } + + const params = new URLSearchParams(queryString); + const encodedBackTo = params.get('backTo'); + + if (!encodedBackTo) { + return undefined; + } + + return encodedBackTo as Route; +} + +function doesRouteTargetCurrentReport(route: string, reportID: string): boolean { + const normalizedRoute = normalizeRoute(route); + const currentReportRoutes = [ + ROUTES.REPORT_WITH_ID.getRoute(reportID), + ROUTES.SEARCH_REPORT.getRoute({reportID}), + ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID}), + ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID}), + ].map(normalizeRoute); + + return currentReportRoutes.some((currentReportRoute) => normalizedRoute === currentReportRoute || normalizedRoute.startsWith(`${currentReportRoute}/`)); +} + +function getBackToOnLeaveReport(reportID: string | undefined, backTo?: Route): Route | undefined { + if (!backTo) { + return undefined; + } + + const normalizedBackTo = normalizeRoute(backTo); + const isSearchRoute = normalizedBackTo.startsWith(ROUTES.SEARCH_ROOT.route); + + if (isSearchRoute) { + if (!reportID || !doesRouteTargetCurrentReport(backTo, reportID)) { + return backTo; + } + + return getNestedBackToRoute(backTo) ?? backTo; + } + + if (!reportID) { + return backTo; + } + + if (doesRouteTargetCurrentReport(backTo, reportID)) { + return undefined; + } + + return backTo; +} + +function shouldUseBackToOnLeaveReport(reportID: string | undefined, backTo?: Route): boolean { + return !!getBackToOnLeaveReport(reportID, backTo); +} + +export {getBackToOnLeaveReport}; +export default shouldUseBackToOnLeaveReport; diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 41afffa75cd1..dbb98441f6dd 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -80,7 +80,9 @@ import Log from '@libs/Log'; import {isEmailPublicDomain} from '@libs/LoginUtils'; import {getMovedReportID} from '@libs/ModifiedExpenseMessage'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; +import getReportRouteForCurrentContext from '@libs/Navigation/helpers/getReportRouteForCurrentContext'; import type {LinkToOptions} from '@libs/Navigation/helpers/linkTo/types'; +import {getBackToOnLeaveReport} from '@libs/Navigation/helpers/shouldUseBackToOnLeaveReport'; import Navigation from '@libs/Navigation/Navigation'; import enhanceParameters from '@libs/Network/enhanceParameters'; import {getDBTimeWithSkew, getIsOffline as isOfflineNetwork} from '@libs/NetworkState'; @@ -200,6 +202,7 @@ import CONFIG from '@src/CONFIG'; import type {OnboardingAccounting} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Route} from '@src/ROUTES'; import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/NewRoomForm'; import type { @@ -2437,7 +2440,7 @@ function navigateToAndOpenChildReport( ) { const report = childReport ?? createChildReport(childReport, parentReportAction, parentReport, currentUserAccountID, introSelected, betas, isSelfTourViewed, personalDetails); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID, undefined, undefined, Navigation.getActiveRoute())); + Navigation.navigate(getReportRouteForCurrentContext({reportID: report.reportID})); } /** @@ -4748,7 +4751,15 @@ function navigateToMostRecentReport( currentUserAccountID: number, introSelected: OnyxEntry, betas: OnyxEntry, + backTo?: Route, ) { + const backToOnLeave = getBackToOnLeaveReport(currentReport?.reportID, backTo); + + if (backToOnLeave) { + Navigation.goBack(backToOnLeave); + return; + } + const lastAccessedReportID = findLastAccessedReport(false, false, currentReport?.reportID)?.reportID; if (lastAccessedReportID) { @@ -4800,6 +4811,7 @@ function leaveGroupChat( conciergeReportID: string | undefined, introSelected: OnyxEntry, betas: OnyxEntry, + backTo?: Route, ) { const reportID = report.reportID; // Use merge instead of set to avoid deleting the report too quickly, which could cause a brief "not found" page to appear. @@ -4847,7 +4859,7 @@ function leaveGroupChat( }, ]; - navigateToMostRecentReport(report, conciergeReportID, currentUserAccountID, introSelected, betas); + navigateToMostRecentReport(report, conciergeReportID, currentUserAccountID, introSelected, betas, backTo); API.write(WRITE_COMMANDS.LEAVE_GROUP_CHAT, {reportID}, {optimisticData, successData, failureData}); } @@ -4859,6 +4871,7 @@ function leaveRoom( introSelected: OnyxEntry, betas: OnyxEntry, isWorkspaceMemberLeavingWorkspaceRoom = false, + backTo?: Route, ) { const reportID = report.reportID; const isChatThread = isChatThreadReportUtils(report); @@ -4963,7 +4976,7 @@ function leaveRoom( return; } // In other cases, the report is deleted and we should move the user to another report. - navigateToMostRecentReport(report, conciergeReportID, currentUserAccountID, introSelected, betas); + navigateToMostRecentReport(report, conciergeReportID, currentUserAccountID, introSelected, betas, backTo); } function buildInviteToRoomOnyxData(report: Report, inviteeEmailsToAccountIDs: InvitedEmailsToAccountIDs, formatPhoneNumber: LocaleContextProps['formatPhoneNumber']) { diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 81b8dde020f1..cb33d2aaa813 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -10,6 +10,7 @@ import {WRITE_COMMANDS} from '@libs/API/types'; import DateUtils from '@libs/DateUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; +import dismissModalForCurrentContext from '@libs/Navigation/helpers/dismissModalForCurrentContext'; import Navigation from '@libs/Navigation/Navigation'; import {getDBTimeWithSkew} from '@libs/NetworkState'; import * as OptionsListUtils from '@libs/OptionsListUtils'; @@ -388,7 +389,7 @@ function createTaskAndNavigate(params: CreateTaskAndNavigateParams) { InteractionManager.runAfterInteractions(() => { clearOutTaskInfo(); }); - Navigation.dismissModalWithReport({reportID: parentReportID}); + dismissModalForCurrentContext(parentReportID); } notifyNewAction(parentReportID, optimisticAddCommentReport.reportAction, true); } @@ -1159,19 +1160,81 @@ function getShareDestination( }; } +function getNestedBackToRoute(route: Route): Route | undefined { + const queryStringIndex = route.indexOf('?'); + const queryString = queryStringIndex === -1 ? '' : route.slice(queryStringIndex + 1); + + if (!queryString) { + return undefined; + } + + const params = new URLSearchParams(queryString); + const encodedBackTo = params.get('backTo'); + + if (!encodedBackTo) { + return undefined; + } + + return encodedBackTo as Route; +} + +function getSearchRouteWithoutReportActionID(route: Route): Route { + const queryStringIndex = route.indexOf('?'); + const path = queryStringIndex === -1 ? route : route.slice(0, queryStringIndex); + const queryString = queryStringIndex === -1 ? '' : route.slice(queryStringIndex + 1); + const match = path.match(/^\/?search\/view\/([^/?]+)\/[^/?]+\/?$/); + + if (!match) { + return route; + } + + const reportID = match.at(1); + if (!reportID) { + return route; + } + + const cleanedRoute = ROUTES.SEARCH_REPORT.getRoute({reportID}); + if (!queryString) { + return cleanedRoute; + } + + return `${cleanedRoute}?${queryString}` as Route; +} + +function getFlattenedTaskDeleteBackTo(backTo: Route): Route { + const flattenedBackTo = getSearchRouteWithoutReportActionID(backTo); + const nestedBackTo = getNestedBackToRoute(flattenedBackTo); + + if (!nestedBackTo) { + return flattenedBackTo; + } + + const flattenedNestedBackTo = getSearchRouteWithoutReportActionID(nestedBackTo); + if (flattenedNestedBackTo === nestedBackTo) { + return flattenedBackTo; + } + + const queryStringIndex = flattenedBackTo.indexOf('?'); + const path = queryStringIndex === -1 ? flattenedBackTo : flattenedBackTo.slice(0, queryStringIndex); + const queryString = queryStringIndex === -1 ? '' : flattenedBackTo.slice(queryStringIndex + 1); + const params = new URLSearchParams(queryString); + params.set('backTo', flattenedNestedBackTo); + + return `${path}?${params.toString()}` as Route; +} + /** * Calculate the URL to navigate to after a task deletion * @param report - The task report being deleted * @returns The URL to navigate to */ -function getNavigationUrlOnTaskDelete(report: OnyxEntry, conciergeReportID: string | undefined): string | undefined { +function getNavigationUrlOnTaskDelete(report: OnyxEntry, conciergeReportID: string | undefined, backTo?: Route): Route | undefined { if (!report) { return undefined; } - const shouldDeleteTaskReport = !ReportActionsUtils.doesReportHaveVisibleActions(report.reportID); - if (!shouldDeleteTaskReport) { - return undefined; + if (backTo) { + return getFlattenedTaskDeleteBackTo(backTo); } if (report?.parentReportID) { @@ -1200,6 +1263,7 @@ function deleteTask( conciergeReportID: string | undefined, delegateEmail: string | undefined, ancestors: ReportUtils.Ancestor[] = [], + backTo?: Route, ) { if (!report) { return; @@ -1324,9 +1388,14 @@ function deleteTask( API.write(WRITE_COMMANDS.CANCEL_TASK, parameters, {optimisticData, successData, failureData}); notifyNewAction(report.reportID, undefined, true); - const urlToNavigateBack = getNavigationUrlOnTaskDelete(report, conciergeReportID); + const shouldNavigateAfterDelete = !!backTo || shouldDeleteTaskReport; + const urlToNavigateBack = shouldNavigateAfterDelete ? getNavigationUrlOnTaskDelete(report, conciergeReportID, backTo) : undefined; if (urlToNavigateBack) { - Navigation.goBack(); + if (backTo) { + Navigation.goBack(urlToNavigateBack, {compareParams: false}); + } else { + Navigation.goBack(); + } return urlToNavigateBack; } } diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index c6c1ef6b9e65..796bba4254ab 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -122,7 +122,7 @@ import { updateGroupChatAvatar, } from '@userActions/Report'; import {callFunctionIfActionIsAllowed} from '@userActions/Session'; -import {canActionTask, canModifyTask, deleteTask, reopenTask} from '@userActions/Task'; +import {canActionTask, canModifyTask, deleteTask, getNavigationUrlOnTaskDelete, reopenTask} from '@userActions/Task'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -352,13 +352,13 @@ function ReportDetailsPage({policy, report, route, reportMetadata, reportLoading const leaveChat = useCallback(() => { if (isRootGroupChat) { - leaveGroupChat(report, quickAction?.chatReportID?.toString() === report.reportID, currentUserPersonalDetails.accountID, conciergeReportID, introSelected, betas); + leaveGroupChat(report, quickAction?.chatReportID?.toString() === report.reportID, currentUserPersonalDetails.accountID, conciergeReportID, introSelected, betas, backTo); return; } const isWorkspaceMemberLeavingWorkspaceRoom = isWorkspaceMemberLeavingWorkspaceRoomUtil(report, isPolicyEmployee, isPolicyAdmin); - leaveRoom(report, currentUserPersonalDetails.accountID, conciergeReportID, introSelected, betas, isWorkspaceMemberLeavingWorkspaceRoom); - }, [isRootGroupChat, isPolicyEmployee, isPolicyAdmin, quickAction?.chatReportID, report, currentUserPersonalDetails.accountID, conciergeReportID, introSelected, betas]); + leaveRoom(report, currentUserPersonalDetails.accountID, conciergeReportID, introSelected, betas, isWorkspaceMemberLeavingWorkspaceRoom, backTo); + }, [isRootGroupChat, isPolicyEmployee, isPolicyAdmin, quickAction?.chatReportID, report, currentUserPersonalDetails.accountID, conciergeReportID, introSelected, betas, backTo]); const showLastMemberLeavingModal = useCallback(async () => { const {action} = await showConfirmModal({ @@ -882,6 +882,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata, reportLoading conciergeReportID, delegateEmail, ancestors, + backTo, ); return; } @@ -941,10 +942,20 @@ function ReportDetailsPage({policy, report, route, reportMetadata, reportLoading removeTransaction, conciergeReportID, delegateEmail, + backTo, ]); // Where to navigate back to after deleting the transaction and its report. const navigateToTargetUrl = useCallback(() => { + if (caseID === CASES.DEFAULT && backTo) { + const urlToNavigateBack = getNavigationUrlOnTaskDelete(report, conciergeReportID, backTo); + + if (urlToNavigateBack) { + setDeleteTransactionNavigateBackUrl(urlToNavigateBack); + } + return; + } + let urlToNavigateBack: string | undefined; // Only proceed with navigation logic if transaction was actually deleted if (!isEmptyObject(requestParentReportAction)) { @@ -1009,7 +1020,20 @@ function ReportDetailsPage({policy, report, route, reportMetadata, reportLoading setDeleteTransactionNavigateBackUrl(urlToNavigateBack); navigateBackOnDeleteTransaction(urlToNavigateBack as Route); } - }, [requestParentReportAction, route.params.reportID, moneyRequestReport, iouTransactionID, iouReport, chatIOUReport, isChatIOUReportArchived, isSingleTransactionView]); + }, [ + backTo, + caseID, + chatIOUReport, + conciergeReportID, + iouReport, + iouTransactionID, + isChatIOUReportArchived, + isSingleTransactionView, + moneyRequestReport, + report, + requestParentReportAction, + route.params.reportID, + ]); const showDeleteModal = useCallback(async () => { const {action} = await showConfirmModal({ @@ -1025,6 +1049,10 @@ function ReportDetailsPage({policy, report, route, reportMetadata, reportLoading } const shouldOpenSplitExpenseEditFlow = iouTransactionID ? shouldOpenSplitExpenseEditFlowOnDelete([iouTransactionID]) : false; Navigation.setNavigationActionToMicrotaskQueue(() => { + if (caseID === CASES.DEFAULT && backTo) { + deleteTransaction(); + return; + } if (shouldOpenSplitExpenseEditFlow) { deleteTransaction(); return; @@ -1037,7 +1065,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata, reportLoading deleteTransaction(); }); }); - }, [showConfirmModal, translate, caseID, iouTransactionID, shouldOpenSplitExpenseEditFlowOnDelete, navigateToTargetUrl, deleteTransaction]); + }, [showConfirmModal, translate, caseID, iouTransactionID, shouldOpenSplitExpenseEditFlowOnDelete, navigateToTargetUrl, deleteTransaction, backTo]); const mentionReportContextValue = useMemo(() => ({currentReportID: report.reportID, exactlyMatch: true}), [report.reportID]); diff --git a/src/pages/inbox/ReportNavigateAwayHandler.tsx b/src/pages/inbox/ReportNavigateAwayHandler.tsx index b76d6e84a8c7..93388e16d57c 100644 --- a/src/pages/inbox/ReportNavigateAwayHandler.tsx +++ b/src/pages/inbox/ReportNavigateAwayHandler.tsx @@ -9,6 +9,7 @@ import useParentReportAction from '@hooks/useParentReportAction'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; +import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import {isDeletedParentAction} from '@libs/ReportActionsUtils'; @@ -212,6 +213,10 @@ function ReportNavigateAwayHandler() { // Fallback to Concierge Navigation.isNavigationReady().then(() => { + if (isSearchTopmostFullScreenRoute()) { + Navigation.dismissModal(); + return; + } navigateToConciergeChat(conciergeReportID, introSelected, currentUserAccountID, isSelfTourViewed, betas); }); }, [reportWasDeleted, isFocused, deletedReportParentID, conciergeReportID, introSelected, currentUserAccountID, isSelfTourViewed, betas]); diff --git a/src/pages/inbox/report/actionContents/ChatTransactionPreview.tsx b/src/pages/inbox/report/actionContents/ChatTransactionPreview.tsx index ffde941139d1..dad4e01e7378 100644 --- a/src/pages/inbox/report/actionContents/ChatTransactionPreview.tsx +++ b/src/pages/inbox/report/actionContents/ChatTransactionPreview.tsx @@ -7,6 +7,7 @@ import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import getReportRouteForCurrentContext from '@libs/Navigation/helpers/getReportRouteForCurrentContext'; import Navigation from '@libs/Navigation/Navigation'; import {getIOUReportIDFromReportActionPreview, isSplitBillAction, isTrackExpenseAction} from '@libs/ReportActionsUtils'; import {createTransactionThreadReport} from '@userActions/Report'; @@ -79,13 +80,13 @@ function ChatTransactionPreview({action, reportID, originalReportID, chatReportI iouReportAction: action, }); if (createdTransactionThreadReport?.reportID) { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(createdTransactionThreadReport.reportID, undefined, undefined, Navigation.getActiveRoute())); + Navigation.navigate(getReportRouteForCurrentContext({reportID: createdTransactionThreadReport.reportID})); return; } return; } - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(action.childReportID, undefined, undefined, Navigation.getActiveRoute())); + Navigation.navigate(getReportRouteForCurrentContext({reportID: action.childReportID})); }} isTrackExpense={isTrackExpenseAction(action)} originalReportID={originalReportID} diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index 60b1409f9580..17ee5ac00d53 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -88,7 +88,7 @@ function TaskAssigneeSelectorModal() { const reportOnyx = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${route.params?.reportID}`]; if (reportOnyx && !isTaskReport(reportOnyx)) { Navigation.isNavigationReady().then(() => { - Navigation.dismissModalWithReport({reportID: reportOnyx.reportID}); + Navigation.goBack(backTo); }); } return reports?.[`${ONYXKEYS.COLLECTION.REPORT}${route.params?.reportID}`]; @@ -183,7 +183,7 @@ function TaskAssigneeSelectorModal() { }); } InteractionManager.runAfterInteractions(() => { - Navigation.dismissModalWithReport({reportID: report?.reportID}); + Navigation.goBack(backTo); }); // If there's no report, we're creating a new task } else if (option.accountID) { diff --git a/src/pages/tasks/TaskDescriptionPage.tsx b/src/pages/tasks/TaskDescriptionPage.tsx index 7c15678b12f4..6a8542bd02be 100644 --- a/src/pages/tasks/TaskDescriptionPage.tsx +++ b/src/pages/tasks/TaskDescriptionPage.tsx @@ -62,14 +62,14 @@ function TaskDescriptionPage({report, currentUserPersonalDetails}: TaskDescripti editTask(report, {description: values.description}, delegateEmail); } - Navigation.dismissModalWithReport({reportID: report?.reportID}); + Navigation.goBack(route.params.backTo); }, - [report, delegateEmail], + [report, delegateEmail, route.params.backTo], ); if (!isTaskReport(report)) { Navigation.isNavigationReady().then(() => { - Navigation.dismissModalWithReport({reportID: report?.reportID}); + Navigation.goBack(route.params.backTo); }); } const inputRef = useRef(null); diff --git a/src/pages/tasks/TaskTitlePage.tsx b/src/pages/tasks/TaskTitlePage.tsx index 0dd7c78efcc3..59dfc3e09090 100644 --- a/src/pages/tasks/TaskTitlePage.tsx +++ b/src/pages/tasks/TaskTitlePage.tsx @@ -67,14 +67,14 @@ function TaskTitlePage({report, currentUserPersonalDetails}: TaskTitlePageProps) editTask(report, {title: values.title}, delegateEmail); } - Navigation.dismissModalWithReport({reportID: report?.reportID}); + Navigation.goBack(route.params.backTo); }, - [report, delegateEmail], + [report, delegateEmail, route.params.backTo], ); if (!isTaskReport(report)) { Navigation.isNavigationReady().then(() => { - Navigation.dismissModalWithReport({reportID: report?.reportID}); + Navigation.goBack(route.params.backTo); }); } diff --git a/tests/actions/TaskTest.ts b/tests/actions/TaskTest.ts index fa6eade5bea5..c387769bb984 100644 --- a/tests/actions/TaskTest.ts +++ b/tests/actions/TaskTest.ts @@ -27,6 +27,7 @@ import initOnyxDerivedValues from '@userActions/OnyxDerived'; import CONST from '@src/CONST'; import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Route} from '@src/ROUTES'; import type {Report, ReportAction} from '@src/types/onyx'; import type {OnyxData} from '@src/types/onyx/Request'; import {getFakeReport, getFakeReportAction} from '../utils/LHNTestUtils'; @@ -1145,11 +1146,20 @@ describe('actions/Task', () => { expect(getNavigationUrlOnTaskDelete(undefined, 'concierge_123')).toBeUndefined(); }); - it('should return undefined when report has visible actions (should not delete)', () => { + it('should return parent report route when report has visible actions and a parent report exists', () => { + const parentReportID = 'parent_123'; + const taskReport = {...getFakeReport(), parentReportID}; + doesReportHaveVisibleActionsSpy.mockReturnValue(true); + + expect(getNavigationUrlOnTaskDelete(taskReport, 'concierge_123')).toBe(`r/${parentReportID}`); + }); + + it('should prefer backTo when report has visible actions', () => { const taskReport = getFakeReport(); + const backTo = '/search' as Route; doesReportHaveVisibleActionsSpy.mockReturnValue(true); - expect(getNavigationUrlOnTaskDelete(taskReport, 'concierge_123')).toBeUndefined(); + expect(getNavigationUrlOnTaskDelete(taskReport, 'concierge_123', backTo)).toBe(backTo); }); it('should return parent report route when report has parentReportID and no visible actions', () => { @@ -1161,6 +1171,16 @@ describe('actions/Task', () => { expect(result).toBe(`r/${parentReportID}`); }); + it('should prefer backTo when report has no visible actions', () => { + const parentReportID = 'parent_123'; + const taskReport = {...getFakeReport(), parentReportID}; + const backTo = '/search' as Route; + doesReportHaveVisibleActionsSpy.mockReturnValue(false); + + const result = getNavigationUrlOnTaskDelete(taskReport, 'concierge_123', backTo); + expect(result).toBe(backTo); + }); + it('should return most recent report route when no parentReportID and getMostRecentReportID returns a value', () => { const taskReport = {...getFakeReport(), parentReportID: undefined}; const mostRecentReportID = 'recent_456'; @@ -1491,7 +1511,41 @@ describe('actions/Task', () => { const result = deleteTask(taskReport, parentReport, false, mockCurrentUserAccountID, false, undefined, 'concierge_123', undefined); expect(result).toBe(`r/${parentReportID}`); - expect(Navigation.goBack).toHaveBeenCalled(); + expect(Navigation.goBack).toHaveBeenCalledWith(); + }); + + it('should navigate back to backTo when provided and the task report is deleted', async () => { + const taskReportID = 'task_report_delete_back_to'; + const parentReportID = 'parent_report_delete_back_to'; + const backTo = '/search' as Route; + + const taskReport = { + reportID: taskReportID, + type: CONST.REPORT.TYPE.TASK, + reportName: 'Task From Search', + parentReportID, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + ownerAccountID: mockCurrentUserAccountID, + }; + + const parentReport = { + reportID: parentReportID, + type: CONST.REPORT.TYPE.CHAT, + }; + + await act(async () => { + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`, taskReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`, parentReport); + }); + await waitForBatchedUpdatesWithAct(); + + doesReportHaveVisibleActionsSpy.mockReturnValue(false); + + const result = deleteTask(taskReport, parentReport, false, mockCurrentUserAccountID, false, undefined, 'concierge_123', undefined, [], backTo); + + expect(result).toBe(backTo); + expect(Navigation.goBack).toHaveBeenCalledWith(backTo, {compareParams: false}); }); it('should return conciergeReportID-based URL when no parentReportID and no recent report', async () => { @@ -1520,10 +1574,10 @@ describe('actions/Task', () => { expect(result).toBe(`r/${conciergeReportID}`); expect(getMostRecentReportIDSpy).toHaveBeenCalledWith(taskReport, conciergeReportID); - expect(Navigation.goBack).toHaveBeenCalled(); + expect(Navigation.goBack).toHaveBeenCalledWith(); }); - it('should not return navigation URL when task report has visible actions', async () => { + it('should navigate away when task report has visible actions', async () => { const taskReportID = 'task_report_delete_4'; const parentReportID = 'parent_report_delete_4'; @@ -1548,7 +1602,7 @@ describe('actions/Task', () => { }); await waitForBatchedUpdatesWithAct(); - // Has visible actions, so should not navigate away + // When the task still has visible actions and there's no backTo, stay on the deleted task page. doesReportHaveVisibleActionsSpy.mockReturnValue(true); const result = deleteTask(taskReport, parentReport, false, mockCurrentUserAccountID, false, undefined, 'concierge_123', undefined); diff --git a/tests/unit/Navigation/shouldUseBackToOnLeaveReport.test.ts b/tests/unit/Navigation/shouldUseBackToOnLeaveReport.test.ts new file mode 100644 index 000000000000..e13b177fbdf3 --- /dev/null +++ b/tests/unit/Navigation/shouldUseBackToOnLeaveReport.test.ts @@ -0,0 +1,33 @@ +import shouldUseBackToOnLeaveReport, {getBackToOnLeaveReport} from '@libs/Navigation/helpers/shouldUseBackToOnLeaveReport'; +import ROUTES from '@src/ROUTES'; + +describe('shouldUseBackToOnLeaveReport', () => { + const reportID = '123'; + + it('preserves a Search route even when it targets the current report', () => { + const backTo = ROUTES.SEARCH_REPORT.getRoute({ + reportID, + reportActionID: '456', + backTo: ROUTES.SEARCH_ROOT.getRoute({query: 'type:chat', rawQuery: 'type:chat'}), + }); + + expect(shouldUseBackToOnLeaveReport(reportID, backTo)).toBe(true); + }); + + it('unwraps nested Search backTo when the immediate Search route targets the current report', () => { + const nestedBackTo = ROUTES.SEARCH_ROOT.getRoute({query: 'type:chat', rawQuery: 'type:chat'}); + const backTo = ROUTES.SEARCH_REPORT.getRoute({ + reportID, + reportActionID: '456', + backTo: nestedBackTo, + }); + + expect(getBackToOnLeaveReport(reportID, backTo)).toBe(nestedBackTo); + }); + + it('does not preserve an Inbox route that points back to the current report', () => { + const backTo = ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID, ROUTES.REPORT_WITH_ID.getRoute(reportID)); + + expect(shouldUseBackToOnLeaveReport(reportID, backTo)).toBe(false); + }); +});