Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 20 additions & 15 deletions src/hooks/useRestoreWorkspacesTabOnNavigate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,27 +35,31 @@ function useRestoreWorkspacesTabOnNavigate() {
return {};
}

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

if (workspaceNavigatorRoute) {
const workspaceNavigatorState = workspaceNavigatorRoute.state ?? (workspaceNavigatorRoute.key ? getPreservedNavigatorState(workspaceNavigatorRoute.key) : undefined);
const lastWorkspaceRoute = workspaceNavigatorState?.routes?.findLast((route) => isWorkspaceNavigatorRouteName(route.name));
if (lastWorkspaceRoute) {
const tabState = lastWorkspaceRoute.state ?? (lastWorkspaceRoute.key ? getPreservedNavigatorState(lastWorkspaceRoute.key) : undefined);
return {lastWorkspacesTabNavigatorRoute: lastWorkspaceRoute, workspacesTabState: tabState, topmostFullScreenRoute};
}
return {topmostFullScreenRoute};
if (lastWorkspaceRoute) {
const tabState = lastWorkspaceRoute.state ?? (lastWorkspaceRoute.key ? getPreservedNavigatorState(lastWorkspaceRoute.key) : undefined);
return {lastWorkspacesTabNavigatorRoute: lastWorkspaceRoute, workspacesTabState: tabState, topmostFullScreenRoute};
}

// Fall back to session storage when no route exists in the navigation tree
// Fall back to session storage when no workspace route exists anywhere in the navigation tree.
const sessionRoute = getWorkspacesTabStateFromSessionStorage()
?.routes?.findLast((route) => route.name === NAVIGATORS.WORKSPACE_NAVIGATOR)
?.state?.routes?.findLast((route) => isWorkspaceNavigatorRouteName(route.name));
if (sessionRoute) {
return {lastWorkspacesTabNavigatorRoute: sessionRoute, workspacesTabState: sessionRoute.state};
return {lastWorkspacesTabNavigatorRoute: sessionRoute, workspacesTabState: sessionRoute.state, topmostFullScreenRoute};
}

return {topmostFullScreenRoute};
Expand Down Expand Up @@ -101,8 +105,9 @@ function useRestoreWorkspacesTabOnNavigate() {
domain: lastViewedDomain,
lastWorkspacesTabNavigatorRoute,
topmostFullScreenRoute,
workspacesTabState,
});
}, [shouldUseNarrowLayout, currentUserLogin, lastViewedPolicy, lastViewedDomain, lastWorkspacesTabNavigatorRoute, topmostFullScreenRoute]);
}, [shouldUseNarrowLayout, currentUserLogin, lastViewedPolicy, lastViewedDomain, lastWorkspacesTabNavigatorRoute, topmostFullScreenRoute, workspacesTabState]);
}

export default useRestoreWorkspacesTabOnNavigate;
45 changes: 40 additions & 5 deletions src/libs/Navigation/helpers/navigateToWorkspacesPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,42 @@ import navigationRef from '@libs/Navigation/navigationRef';
import {isPendingDeletePolicy, shouldShowPolicy as shouldShowPolicyUtil} from '@libs/PolicyUtils';
import NAVIGATORS from '@src/NAVIGATORS';
import ROUTES from '@src/ROUTES';
import type {Route} from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import type {Domain, Policy} from '@src/types/onyx';
import getActiveTabName from './getActiveTabName';
import getPathFromState from './getPathFromState';

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

/**
* Wraps a leaf navigation state in successive ancestor navigators (outermost first).
* Used to reconstruct the linking-config hierarchy that `getPathFromState` walks when
* resolving a state subtree to a URL.
*/
function wrapStateInNavigators(state: PartialState<NavigationState>, navigators: readonly string[]): PartialState<NavigationState> {
return navigators.reduceRight<PartialState<NavigationState>>((acc, name) => ({routes: [{name, state: acc}], index: 0}), state);
}

type Params = {
currentUserLogin?: string;
shouldUseNarrowLayout: boolean;
policy?: Policy;
domain?: Domain;
lastWorkspacesTabNavigatorRoute?: RouteType;
topmostFullScreenRoute?: RouteType;
/**
* The full WorkspaceSplitNavigator inner state captured by the hook.
* Wrapped in a synthetic outer node and fed to `getPathFromState` to reconstruct
* the deep URL the user was on (e.g. `/workspaces/POLICY_ID/workflows`). Navigating
* via that URL goes through `getStateFromPath` which produces a fully-formed
* navigation state — bypassing custom router actions that don't seed nested state
* when pushing a fresh TabNavigator on top of an existing fullscreen stack.
*/
workspacesTabState?: NavigationState | PartialState<NavigationState>;
};

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

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