Skip to content

Commit 309c5a3

Browse files
luacmartinsOSBotify
authored andcommitted
Merge pull request #89106 from software-mansion-labs/@adamgrzybowski/fix-workspaces-tab-reset-89009
Fix issue: Expensify Card - After clicking View transactions, Workspaces tab resets to initial page (cherry picked from commit 266c03a) (cherry-picked to staging by luacmartins)
1 parent a32ce35 commit 309c5a3

4 files changed

Lines changed: 330 additions & 26 deletions

File tree

src/hooks/useRestoreWorkspacesTabOnNavigate.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,31 @@ function useRestoreWorkspacesTabOnNavigate() {
3535
return {};
3636
}
3737

38-
// Look inside TabNavigator for WORKSPACE_NAVIGATOR
39-
const rootTabRoute = rootState?.routes.findLast((route) => route.name === NAVIGATORS.TAB_NAVIGATOR);
40-
const rootTabState = getTabState(rootTabRoute);
41-
const workspaceNavigatorRoute = rootTabState?.routes?.find((route) => route.name === NAVIGATORS.WORKSPACE_NAVIGATOR);
38+
// Multiple TAB_NAVIGATOR instances can coexist in the root stack — when navigation from
39+
// inside an RHP targets a tab, linkTo PUSHes a fresh TabNavigator above the modal, and that
40+
// new instance's WORKSPACE_NAVIGATOR slot starts empty. Older instances kept alive by
41+
// ensureTabNavigatorRoutes still hold the previous workspace state, so flatten every
42+
// workspace route from every TabNavigator in stack order and take the most recent one.
43+
const lastWorkspaceRoute = (rootState?.routes ?? [])
44+
.filter((route) => route.name === NAVIGATORS.TAB_NAVIGATOR)
45+
.flatMap((tabNavigatorRoute) => {
46+
const workspaceNavigatorRoute = getTabState(tabNavigatorRoute)?.routes?.find((route) => route.name === NAVIGATORS.WORKSPACE_NAVIGATOR);
47+
const workspaceNavigatorState = workspaceNavigatorRoute?.state ?? (workspaceNavigatorRoute?.key ? getPreservedNavigatorState(workspaceNavigatorRoute.key) : undefined);
48+
return workspaceNavigatorState?.routes?.filter((route) => isWorkspaceNavigatorRouteName(route.name)) ?? [];
49+
})
50+
.at(-1);
4251

43-
if (workspaceNavigatorRoute) {
44-
const workspaceNavigatorState = workspaceNavigatorRoute.state ?? (workspaceNavigatorRoute.key ? getPreservedNavigatorState(workspaceNavigatorRoute.key) : undefined);
45-
const lastWorkspaceRoute = workspaceNavigatorState?.routes?.findLast((route) => isWorkspaceNavigatorRouteName(route.name));
46-
if (lastWorkspaceRoute) {
47-
const tabState = lastWorkspaceRoute.state ?? (lastWorkspaceRoute.key ? getPreservedNavigatorState(lastWorkspaceRoute.key) : undefined);
48-
return {lastWorkspacesTabNavigatorRoute: lastWorkspaceRoute, workspacesTabState: tabState, topmostFullScreenRoute};
49-
}
50-
return {topmostFullScreenRoute};
52+
if (lastWorkspaceRoute) {
53+
const tabState = lastWorkspaceRoute.state ?? (lastWorkspaceRoute.key ? getPreservedNavigatorState(lastWorkspaceRoute.key) : undefined);
54+
return {lastWorkspacesTabNavigatorRoute: lastWorkspaceRoute, workspacesTabState: tabState, topmostFullScreenRoute};
5155
}
5256

53-
// Fall back to session storage when no route exists in the navigation tree
57+
// Fall back to session storage when no workspace route exists anywhere in the navigation tree.
5458
const sessionRoute = getWorkspacesTabStateFromSessionStorage()
5559
?.routes?.findLast((route) => route.name === NAVIGATORS.WORKSPACE_NAVIGATOR)
5660
?.state?.routes?.findLast((route) => isWorkspaceNavigatorRouteName(route.name));
5761
if (sessionRoute) {
58-
return {lastWorkspacesTabNavigatorRoute: sessionRoute, workspacesTabState: sessionRoute.state};
62+
return {lastWorkspacesTabNavigatorRoute: sessionRoute, workspacesTabState: sessionRoute.state, topmostFullScreenRoute};
5963
}
6064

6165
return {topmostFullScreenRoute};
@@ -101,8 +105,9 @@ function useRestoreWorkspacesTabOnNavigate() {
101105
domain: lastViewedDomain,
102106
lastWorkspacesTabNavigatorRoute,
103107
topmostFullScreenRoute,
108+
workspacesTabState,
104109
});
105-
}, [shouldUseNarrowLayout, currentUserLogin, lastViewedPolicy, lastViewedDomain, lastWorkspacesTabNavigatorRoute, topmostFullScreenRoute]);
110+
}, [shouldUseNarrowLayout, currentUserLogin, lastViewedPolicy, lastViewedDomain, lastWorkspacesTabNavigatorRoute, topmostFullScreenRoute, workspacesTabState]);
106111
}
107112

108113
export default useRestoreWorkspacesTabOnNavigate;

src/libs/Navigation/helpers/navigateToWorkspacesPage.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,42 @@ import navigationRef from '@libs/Navigation/navigationRef';
66
import {isPendingDeletePolicy, shouldShowPolicy as shouldShowPolicyUtil} from '@libs/PolicyUtils';
77
import NAVIGATORS from '@src/NAVIGATORS';
88
import ROUTES from '@src/ROUTES';
9+
import type {Route} from '@src/ROUTES';
910
import SCREENS from '@src/SCREENS';
1011
import type {Domain, Policy} from '@src/types/onyx';
1112
import getActiveTabName from './getActiveTabName';
13+
import getPathFromState from './getPathFromState';
1214

1315
type RouteType = NavigationState['routes'][number] | PartialState<NavigationState>['routes'][number];
1416

17+
/**
18+
* Wraps a leaf navigation state in successive ancestor navigators (outermost first).
19+
* Used to reconstruct the linking-config hierarchy that `getPathFromState` walks when
20+
* resolving a state subtree to a URL.
21+
*/
22+
function wrapStateInNavigators(state: PartialState<NavigationState>, navigators: readonly string[]): PartialState<NavigationState> {
23+
return navigators.reduceRight<PartialState<NavigationState>>((acc, name) => ({routes: [{name, state: acc}], index: 0}), state);
24+
}
25+
1526
type Params = {
1627
currentUserLogin?: string;
1728
shouldUseNarrowLayout: boolean;
1829
policy?: Policy;
1930
domain?: Domain;
2031
lastWorkspacesTabNavigatorRoute?: RouteType;
2132
topmostFullScreenRoute?: RouteType;
33+
/**
34+
* The full WorkspaceSplitNavigator inner state captured by the hook.
35+
* Wrapped in a synthetic outer node and fed to `getPathFromState` to reconstruct
36+
* the deep URL the user was on (e.g. `/workspaces/POLICY_ID/workflows`). Navigating
37+
* via that URL goes through `getStateFromPath` which produces a fully-formed
38+
* navigation state — bypassing custom router actions that don't seed nested state
39+
* when pushing a fresh TabNavigator on top of an existing fullscreen stack.
40+
*/
41+
workspacesTabState?: NavigationState | PartialState<NavigationState>;
2242
};
2343

24-
// Navigates to the appropriate workspace tab or workspace list page.
25-
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- shouldUseNarrowLayout kept for API compat with callers
26-
const navigateToWorkspacesPage = ({currentUserLogin, shouldUseNarrowLayout, policy, domain, lastWorkspacesTabNavigatorRoute, topmostFullScreenRoute}: Params) => {
44+
const navigateToWorkspacesPage = ({currentUserLogin, shouldUseNarrowLayout, policy, domain, lastWorkspacesTabNavigatorRoute, topmostFullScreenRoute, workspacesTabState}: Params) => {
2745
const rootState = navigationRef.getRootState();
2846
const focusedRoute = rootState ? findFocusedRoute(rootState) : undefined;
2947
const isOnWorkspacesList = focusedRoute?.name === SCREENS.WORKSPACES_LIST;
@@ -61,9 +79,26 @@ const navigateToWorkspacesPage = ({currentUserLogin, shouldUseNarrowLayout, poli
6179
return;
6280
}
6381

64-
// Restore to last-visited workspace — navigate through standard routing which switches the tab
6582
if (policy?.id) {
66-
Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policy.id));
83+
// Synthesize a URL from the captured WorkspaceSplitNavigator inner state and navigate
84+
// to it. URL-based navigation goes through `getStateFromPath`, which produces a fully
85+
// formed nested state and reliably handles pushing a fresh TabNavigator on top of an
86+
// existing fullscreen stack. The state has to be wrapped with its full ancestor chain
87+
// (TAB_NAVIGATOR > WORKSPACE_NAVIGATOR > WORKSPACE_SPLIT_NAVIGATOR) so `getPathFromState`
88+
// can match the linking-config hierarchy and produce a real URL like
89+
// `/workspaces/POLICY_ID/workflows`; otherwise the resolver falls back to navigator
90+
// names as path segments and the result hits 404. Narrow layouts skip the deep-restore
91+
// and go to the workspace's initial page (mirrors mobile behavior).
92+
const wrappedState =
93+
!shouldUseNarrowLayout && workspacesTabState
94+
? wrapStateInNavigators(workspacesTabState as PartialState<NavigationState>, [
95+
NAVIGATORS.TAB_NAVIGATOR,
96+
NAVIGATORS.WORKSPACE_NAVIGATOR,
97+
NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR,
98+
])
99+
: undefined;
100+
const targetPath = (wrappedState ? getPathFromState(wrappedState) : ROUTES.WORKSPACE_INITIAL.getRoute(policy.id)) as Route;
101+
Navigation.navigate(targetPath);
67102
}
68103
return;
69104
}

0 commit comments

Comments
 (0)