Skip to content

Commit b382d67

Browse files
authored
Merge pull request #88527 from software-mansion-labs/fix/open-report-thread-in-rhp
Navigate to report thread in RHP when viewing a report in a right docked modal
2 parents c262600 + a6205c7 commit b382d67

9 files changed

Lines changed: 156 additions & 34 deletions

File tree

src/components/ParentNavigationSubtitle.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -182,16 +182,22 @@ function ParentNavigationSubtitle({
182182
// avoid stacking RHPs by going back to the search report if it's already there
183183
const previousRoute = currentFocusedNavigator?.state?.routes.at(-2);
184184

185-
if (previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT && lastRoute?.name === SCREENS.RIGHT_MODAL.EXPENSE_REPORT) {
186-
if (previousRoute.params && 'reportID' in previousRoute.params) {
187-
const reportIDFromParams = previousRoute.params.reportID;
185+
if (previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT && previousRoute.params && 'reportID' in previousRoute.params) {
186+
const reportIDFromParams = previousRoute.params.reportID;
188187

189-
if (reportIDFromParams === parentReportID) {
190-
Navigation.goBack();
191-
return;
192-
}
188+
if (reportIDFromParams === parentReportID) {
189+
Navigation.goBack();
190+
return;
193191
}
194192
}
193+
194+
// Stay in the Search tab when the parent link is tapped from a SEARCH_REPORT RHP
195+
// and the parent isn't already in the stack — otherwise the REPORT_WITH_ID fallback
196+
// would yank the user to Inbox.
197+
if (isReportInRHP) {
198+
Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: parentReportID, reportActionID: isVisibleAction ? parentReportActionID : undefined}));
199+
return;
200+
}
195201
}
196202

197203
// If the parent report is already the previous screen in the main stack, go back to it

src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {
108108
const containerRef = useRef(null);
109109
const isExecutingRef = useRef<boolean>(false);
110110
const screenOptions = useRHPScreenOptions();
111-
const {superWideRHPRouteKeys, shouldRenderTertiaryOverlay} = useWideRHPState();
111+
const {superWideRHPRouteKeys, wideRHPRouteKeys, shouldRenderTertiaryOverlay} = useWideRHPState();
112112
const {clearWideRHPKeys, syncRHPKeys} = useWideRHPActions();
113113
const {windowWidth} = useWindowDimensions();
114114
const modalStackScreenOptions = useModalStackScreenOptions();
@@ -133,7 +133,7 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {
133133

134134
// Animation should be disabled when we open the wide rhp from the narrow one.
135135
// When the wide rhp page is opened as first one, it will be animated with the entire RightModalNavigator.
136-
const animationEnabledOnSearchReport = superWideRHPRouteKeys.length > 0 || isSmallScreenWidth;
136+
const animationEnabledOnSearchReport = superWideRHPRouteKeys.length > 0 || wideRHPRouteKeys.length > 0 || isSmallScreenWidth;
137137

138138
const animatedWidth = expandedRHPProgress.interpolate({
139139
inputRange: [0, 1, 2],
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import {navigationRef} from '@libs/Navigation/Navigation';
2+
import type {NavigationRoute, RootNavigatorParamList, State} from '@libs/Navigation/types';
3+
import NAVIGATORS from '@src/NAVIGATORS';
4+
5+
/**
6+
* Returns the active tab route of the topmost TAB_NAVIGATOR in the root navigation state.
7+
* Use this to determine which full-screen tab (Search, Inbox, etc.) is currently focused.
8+
*/
9+
function getTopmostFullScreenRoute(): NavigationRoute | undefined {
10+
const rootState = navigationRef.getRootState() as State<RootNavigatorParamList>;
11+
12+
if (!rootState) {
13+
return undefined;
14+
}
15+
16+
const topmostTabNavigatorRoute = rootState.routes.findLast((route) => route.name === NAVIGATORS.TAB_NAVIGATOR);
17+
if (!topmostTabNavigatorRoute?.state) {
18+
return undefined;
19+
}
20+
const index = topmostTabNavigatorRoute.state.index ?? 0;
21+
return topmostTabNavigatorRoute.state.routes?.at(index);
22+
}
23+
24+
export default getTopmostFullScreenRoute;
Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
1-
import {navigationRef} from '@libs/Navigation/Navigation';
2-
import type {RootNavigatorParamList, State} from '@libs/Navigation/types';
31
import NAVIGATORS from '@src/NAVIGATORS';
4-
import getActiveTabName from './getActiveTabName';
5-
import {isFullScreenName} from './isNavigatorName';
2+
import getTopmostFullScreenRoute from './getTopmostFullScreenRoute';
63

74
const isReportTopmostSplitNavigator = (): boolean => {
8-
const rootState = navigationRef.getRootState() as State<RootNavigatorParamList>;
9-
10-
if (!rootState) {
5+
const topmostFullScreenRoute = getTopmostFullScreenRoute();
6+
if (!topmostFullScreenRoute) {
117
return false;
128
}
13-
14-
const topmostFullScreenRoute = rootState.routes.findLast((route) => isFullScreenName(route.name));
15-
return getActiveTabName(topmostFullScreenRoute) === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR;
9+
return topmostFullScreenRoute.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR;
1610
};
1711

1812
export default isReportTopmostSplitNavigator;
Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
1-
import {navigationRef} from '@libs/Navigation/Navigation';
2-
import type {RootNavigatorParamList, State} from '@libs/Navigation/types';
31
import NAVIGATORS from '@src/NAVIGATORS';
4-
import getActiveTabName from './getActiveTabName';
5-
import {isFullScreenName} from './isNavigatorName';
2+
import getTopmostFullScreenRoute from './getTopmostFullScreenRoute';
63

74
const isSearchTopmostFullScreenRoute = (): boolean => {
8-
const rootState = navigationRef.getRootState() as State<RootNavigatorParamList>;
9-
10-
if (!rootState) {
5+
const topmostFullScreenRoute = getTopmostFullScreenRoute();
6+
if (!topmostFullScreenRoute) {
117
return false;
128
}
13-
14-
const topmostFullScreenRoute = rootState.routes.findLast((route) => isFullScreenName(route.name));
15-
return getActiveTabName(topmostFullScreenRoute) === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR;
9+
return topmostFullScreenRoute.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR;
1610
};
1711

1812
export default isSearchTopmostFullScreenRoute;

src/libs/actions/Report/index.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import Log from '@libs/Log';
7979
import {isEmailPublicDomain} from '@libs/LoginUtils';
8080
import {getMovedReportID} from '@libs/ModifiedExpenseMessage';
8181
import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute';
82+
import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
8283
import type {LinkToOptions} from '@libs/Navigation/helpers/linkTo/types';
8384
import Navigation from '@libs/Navigation/Navigation';
8485
import enhanceParameters from '@libs/Network/enhanceParameters';
@@ -2187,7 +2188,11 @@ function navigateToAndOpenChildReport(
21872188
) {
21882189
const report = childReport ?? createChildReport(childReport, parentReportAction, parentReport, currentUserAccountID, introSelected, betas, isSelfTourViewed, personalDetails);
21892190

2190-
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID, undefined, undefined, Navigation.getActiveRoute()));
2191+
if (isSearchTopmostFullScreenRoute()) {
2192+
Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: report.reportID, backTo: Navigation.getActiveRoute()}));
2193+
} else {
2194+
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID, undefined, undefined, Navigation.getActiveRoute()));
2195+
}
21912196
}
21922197

21932198
/**
@@ -2280,7 +2285,11 @@ function explain(
22802285
// Check if explanation thread report already exists
22812286
const report = childReport ?? createChildReport(childReport, reportAction, originalReport, currentUserAccountID, introSelected, betas, isSelfTourViewed);
22822287

2283-
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID, undefined, undefined, Navigation.getActiveRoute()));
2288+
if (isSearchTopmostFullScreenRoute()) {
2289+
Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: report.reportID, backTo: Navigation.getActiveRoute()}));
2290+
} else {
2291+
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID, undefined, undefined, Navigation.getActiveRoute()));
2292+
}
22842293
// Schedule adding the explanation comment on the next animation frame
22852294
// so it runs immediately after navigation completes.
22862295
requestAnimationFrame(() => {

src/pages/inbox/HeaderView.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ function HeaderView({onNavigationMenuButtonClicked, reportID}: HeaderViewProps)
105105
const {isSmallScreenWidth, shouldUseNarrowLayout, isInLandscapeMode} = useResponsiveLayout();
106106
const isInSidePanel = useIsInSidePanel();
107107
const route = useRoute();
108+
const openParentReportInCurrentTab = route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT;
108109
const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(report?.parentReportID) ?? getNonEmptyStringOnyxID(report?.reportID)}`);
109110
const [grandParentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(parentReport?.parentReportID)}`);
110111
const grandParentReportAction = useParentReportAction(parentReport);
@@ -346,6 +347,7 @@ function HeaderView({onNavigationMenuButtonClicked, reportID}: HeaderViewProps)
346347
parentReportID={parentNavigationReport?.parentReportID}
347348
parentReportActionID={isParentOneTransactionThread ? undefined : parentNavigationReport?.parentReportActionID}
348349
pressableStyles={[styles.alignSelfStart, styles.mw100]}
350+
openParentReportInCurrentTab={openParentReportInCurrentTab}
349351
humanAgentAccountID={humanAgentAccountID}
350352
humanAgentName={humanAgentName}
351353
/>

src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,9 @@ function ComposerWithSuggestions({
266266

267267
const commentRef = useRef(value);
268268

269-
const {superWideRHPRouteKeys} = useWideRHPState();
270-
// When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank
271-
const shouldDelayAutoFocus = superWideRHPRouteKeys.length > 0 && route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT;
269+
const {superWideRHPRouteKeys, wideRHPRouteKeys} = useWideRHPState();
270+
// When SearchReport is stacked above another RHP (wide or super-wide), delay autofocus until after the transition completes to avoid animation jank
271+
const shouldDelayAutoFocus = (superWideRHPRouteKeys.length > 0 || wideRHPRouteKeys.length > 0) && route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT;
272272
const shouldDelayAutoFocusRef = useRef(shouldDelayAutoFocus);
273273
shouldDelayAutoFocusRef.current = shouldDelayAutoFocus;
274274

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import getTopmostFullScreenRoute from '@libs/Navigation/helpers/getTopmostFullScreenRoute';
2+
import NAVIGATORS from '@src/NAVIGATORS';
3+
4+
const mockGetRootState = jest.fn();
5+
6+
jest.mock('@libs/Navigation/Navigation', () => ({
7+
navigationRef: {
8+
getRootState: () => mockGetRootState() as unknown,
9+
},
10+
}));
11+
12+
describe('getTopmostFullScreenRoute', () => {
13+
beforeEach(() => {
14+
mockGetRootState.mockReset();
15+
});
16+
17+
it('returns undefined when there is no root state', () => {
18+
mockGetRootState.mockReturnValue(undefined);
19+
expect(getTopmostFullScreenRoute()).toBeUndefined();
20+
});
21+
22+
it('returns undefined when there is no TAB_NAVIGATOR route in the root state', () => {
23+
mockGetRootState.mockReturnValue({
24+
routes: [{name: NAVIGATORS.RIGHT_MODAL_NAVIGATOR}, {name: NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR}],
25+
});
26+
expect(getTopmostFullScreenRoute()).toBeUndefined();
27+
});
28+
29+
it('returns undefined when the TAB_NAVIGATOR has no nested state yet', () => {
30+
mockGetRootState.mockReturnValue({
31+
routes: [{name: NAVIGATORS.TAB_NAVIGATOR}],
32+
});
33+
expect(getTopmostFullScreenRoute()).toBeUndefined();
34+
});
35+
36+
it('returns the focused tab route based on state.index', () => {
37+
const reportsRoute = {name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR};
38+
const searchRoute = {name: NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR};
39+
mockGetRootState.mockReturnValue({
40+
routes: [
41+
{
42+
name: NAVIGATORS.TAB_NAVIGATOR,
43+
state: {
44+
index: 1,
45+
routes: [reportsRoute, searchRoute],
46+
},
47+
},
48+
],
49+
});
50+
expect(getTopmostFullScreenRoute()).toBe(searchRoute);
51+
});
52+
53+
it('falls back to the first child route when state.index is missing', () => {
54+
const reportsRoute = {name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR};
55+
const searchRoute = {name: NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR};
56+
mockGetRootState.mockReturnValue({
57+
routes: [
58+
{
59+
name: NAVIGATORS.TAB_NAVIGATOR,
60+
state: {
61+
routes: [reportsRoute, searchRoute],
62+
},
63+
},
64+
],
65+
});
66+
expect(getTopmostFullScreenRoute()).toBe(reportsRoute);
67+
});
68+
69+
it('returns the focused tab of the topmost TAB_NAVIGATOR when multiple exist', () => {
70+
const oldFocused = {name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR};
71+
const newFocused = {name: NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR};
72+
mockGetRootState.mockReturnValue({
73+
routes: [
74+
{
75+
name: NAVIGATORS.TAB_NAVIGATOR,
76+
state: {
77+
index: 0,
78+
routes: [oldFocused],
79+
},
80+
},
81+
{name: NAVIGATORS.RIGHT_MODAL_NAVIGATOR},
82+
{
83+
name: NAVIGATORS.TAB_NAVIGATOR,
84+
state: {
85+
index: 0,
86+
routes: [newFocused],
87+
},
88+
},
89+
],
90+
});
91+
expect(getTopmostFullScreenRoute()).toBe(newFocused);
92+
});
93+
});

0 commit comments

Comments
 (0)