Skip to content
Draft
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: 19 additions & 16 deletions src/libs/Navigation/helpers/getAdaptedStateFromPath.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import getInitialSplitNavigatorState from '@libs/Navigation/AppNavigator/createSplitNavigator/getInitialSplitNavigatorState';
import TAB_SCREENS from '@libs/Navigation/AppNavigator/Navigators/TAB_SCREENS';
import {normalizedConfigs} from '@libs/Navigation/linkingConfig/config';
import {
RHP_TO_DOMAIN,
RHP_TO_HOME,
Expand All @@ -10,18 +11,18 @@ import {
RHP_TO_WORKSPACE,
RHP_TO_WORKSPACES_LIST,
} from '@libs/Navigation/linkingConfig/RELATIONS';
import type {NavigationPartialRoute, NavigationRoute, RootNavigatorParamList} from '@libs/Navigation/types';
import type {NavigationPartialRoute, NavigationRoute} from '@libs/Navigation/types';
import {hasKey} from '@libs/ObjectUtils';
import {getReportOrDraftReport} from '@libs/ReportUtils';
import {getSearchParamFromPath} from '@libs/Url';

import NAVIGATORS from '@src/NAVIGATORS';
import type {Route as RoutePath} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import type {Screen} from '@src/SCREENS';

import type {NavigationState, PartialState, getStateFromPath as RNGetStateFromPath, Route} from '@react-navigation/native';

import {findFocusedRoute} from '@react-navigation/native';
import pick from 'lodash/pick';

import buildTabNavigatorNestedState from './buildTabNavigatorNestedState';
Expand All @@ -36,7 +37,6 @@ import getParamsFromRoute from './getParamsFromRoute';
import getStateFromPath from './getStateFromPath';
import {isFullScreenName} from './isNavigatorName';
import normalizePath from './normalizePath';
import replacePathInNestedState from './replacePathInNestedState';

type GetAdaptedStateReturnType = ReturnType<typeof getStateFromPath>;

Expand Down Expand Up @@ -89,14 +89,14 @@ function isRouteWithReportID(route: NavigationRoute): route is Route<string, {re
* without changing the page currently underneath.
*/
function getMatchingFullScreenRoute(route: NavigationRoute, isDeeplink = false) {
const isDynamicScreen = isDynamicRouteScreen(route.name as Screen);
const isDynamicScreen = hasKey(normalizedConfigs, route.name) && isDynamicRouteScreen(route.name);

// Check for backTo param. One screen with different backTo value may need different screens visible under the overlay.
// Dynamic screens are skipped here because they never carry their own backTo - they only
// inherit it from the screen underneath. Letting backTo dictate the full-screen route for
// a dynamic screen would resolve the wrong page.
if (isRouteWithBackToParam(route) && !isDynamicScreen) {
const stateForBackTo = getStateFromPath(route.params.backTo as RoutePath);
const stateForBackTo = getStateFromPath(route.params.backTo);

// This may happen if the backTo url is invalid.
const lastRoute = stateForBackTo?.routes.at(-1);
Expand Down Expand Up @@ -314,20 +314,20 @@ function getOnboardingAdaptedState(state: PartialState<NavigationState>): Partia
return getRoutesWithIndex(routes);
}

function getAdaptedState(state: PartialState<NavigationState<RootNavigatorParamList>>): GetAdaptedStateReturnType {
function getAdaptedState(state: PartialState<NavigationState>): GetAdaptedStateReturnType {
let currentState = state;
const fullScreenRoute = currentState.routes.find((route) => isFullScreenName(route.name));

if (fullScreenRoute?.name === NAVIGATORS.TAB_NAVIGATOR) {
let tabState = fullScreenRoute.state as PartialState<NavigationState> | undefined;
let tabState = fullScreenRoute.state;

// RN's getStateFromPath emits only the tab matched by the path, so the TAB_NAVIGATOR strip may be sparse.
// Rebuild the full strip around the active tab — consumers (e.g. REPLACE_FULLSCREEN_UNDER_RHP) expect every tab to be present.
// Only the active tab's nested state is carried over; any other tabs in a sparse strip are placeholders without state.
if (tabState?.routes && tabState.routes.length < TAB_SCREENS.length) {
const activeTabRoute = tabState.routes.at(tabState.index ?? tabState.routes.length - 1);
if (activeTabRoute) {
tabState = getTabNavigatorState(activeTabRoute as NavigationPartialRoute).state;
tabState = getTabNavigatorState(activeTabRoute).state;
const normalizedRoutes = currentState.routes.map((r) => (r === fullScreenRoute ? {...r, state: tabState} : r));
currentState = {...currentState, routes: normalizedRoutes};
}
Expand All @@ -336,19 +336,19 @@ function getAdaptedState(state: PartialState<NavigationState<RootNavigatorParamL
// If TAB_NAVIGATOR contains WORKSPACE_NAVIGATOR, ensure WORKSPACES_LIST is in its nested state
const wsNavRoute = tabState?.routes?.find((r) => r.name === NAVIGATORS.WORKSPACE_NAVIGATOR);
if (wsNavRoute) {
const wsNavState = wsNavRoute.state as PartialState<NavigationState> | undefined;
const wsNavState = wsNavRoute.state;
const hasWorkspacesList = wsNavState?.routes?.some((r) => r.name === SCREENS.WORKSPACES_LIST);

if (!hasWorkspacesList && wsNavState?.routes?.length) {
const updatedNestedState = getRoutesWithIndex([{name: SCREENS.WORKSPACES_LIST}, ...(wsNavState.routes ?? [])]);
const updatedWsNavRoute = {...wsNavRoute, state: updatedNestedState};
const updatedTabRoutes = (tabState?.routes ?? []).map((r) => (r.name === NAVIGATORS.WORKSPACE_NAVIGATOR ? updatedWsNavRoute : r)) as NavigationPartialRoute[];
const updatedTabRoutes = (tabState?.routes ?? []).map((r) => (r.name === NAVIGATORS.WORKSPACE_NAVIGATOR ? updatedWsNavRoute : r));
const updatedTabState = {...tabState, routes: updatedTabRoutes};
const updatedFullScreenRoute = {
...fullScreenRoute,
state: updatedTabState,
};
const updatedRoutes = currentState.routes.map((r) => (r.name === NAVIGATORS.TAB_NAVIGATOR ? updatedFullScreenRoute : r)) as NavigationPartialRoute[];
const updatedRoutes = currentState.routes.map((r) => (r.name === NAVIGATORS.TAB_NAVIGATOR ? updatedFullScreenRoute : r));
return getRoutesWithIndex(updatedRoutes);
}
}
Expand All @@ -358,8 +358,8 @@ function getAdaptedState(state: PartialState<NavigationState<RootNavigatorParamL
if (!fullScreenRoute) {
const focusedRoute = findFocusedRouteWithOnyxTabGuard(currentState);

if (focusedRoute?.path && isDynamicRouteScreen(focusedRoute.name as Screen)) {
currentState = getDynamicRouteAdaptedState(currentState, focusedRoute.path) as PartialState<NavigationState<RootNavigatorParamList>>;
if (focusedRoute?.path && hasKey(normalizedConfigs, focusedRoute.name) && isDynamicRouteScreen(focusedRoute.name)) {
currentState = getDynamicRouteAdaptedState(currentState, focusedRoute.path);

// getDynamicRouteAdaptedState may have already resolved the full screen route.
// In that case, skip the default full screen route injection below - the state is already complete.
Expand Down Expand Up @@ -432,9 +432,12 @@ const getAdaptedStateFromPath: GetAdaptedStateFromPath = (path, options, shouldR
let normalizedPath = !path.startsWith('/') ? `/${path}` : path;
normalizedPath = getMatchingNewRoute(normalizedPath) ?? normalizedPath;

const state = getStateFromPath(normalizedPath as RoutePath) as PartialState<NavigationState<RootNavigatorParamList>>;
const state = getStateFromPath(normalizedPath);
if (shouldReplacePathInNestedState) {
replacePathInNestedState(state, normalizedPath);
const focusedRoute = findFocusedRoute(state);
if (focusedRoute) {
focusedRoute.path = normalizedPath;
}
}

if (state === undefined) {
Expand Down
49 changes: 31 additions & 18 deletions src/libs/Navigation/helpers/getPathFromState.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Log from '@libs/Log';
import {config, normalizedConfigs, screensWithOnyxTabNavigator} from '@libs/Navigation/linkingConfig/config';
import type {State} from '@libs/Navigation/types';
import {hasKey} from '@libs/ObjectUtils';

import type {Screen} from '@src/SCREENS';
import type {NavigationState, PartialState} from '@react-navigation/native';

import {getPathFromState as RNGetPathFromState} from '@react-navigation/native';

Expand All @@ -11,10 +12,6 @@ import isDynamicRouteScreen from './dynamicRoutesUtils/isDynamicRouteScreen';
import splitPathAndQuery from './dynamicRoutesUtils/splitPathAndQuery';
import findFocusedRouteWithOnyxTabGuard from './findFocusedRouteWithOnyxTabGuard';

function isScreen(name: string): name is Screen {
return name in normalizedConfigs;
}

/**
* Resolves a single path segment: if it's a `:param` placeholder, replaces it
* with the URL-encoded value from `params`; otherwise returns the segment as-is.
Expand Down Expand Up @@ -80,6 +77,9 @@ function buildSuffixFromPattern(pattern: string, params: Record<string, unknown>
*
* @private - Internal helper. Do not export or use outside this file.
*/
function popFocusedRoute(state: NavigationState): NavigationState | undefined;
function popFocusedRoute(state: PartialState<NavigationState>): PartialState<NavigationState> | undefined;
function popFocusedRoute(state: State): State | undefined;
function popFocusedRoute(state: State): State | undefined {
const index = state.index ?? state.routes.length - 1;
const focusedRoute = state.routes[index];
Expand All @@ -92,21 +92,34 @@ function popFocusedRoute(state: State): State | undefined {
// the focused route has nested state - try to pop from deeper levels first,
// unless it hosts an OnyxTabNavigator (treat it as a leaf).
if (focusedRoute.state && focusedRoute.name && !screensWithOnyxTabNavigator.has(focusedRoute.name)) {
const nestedResult = popFocusedRoute(focusedRoute.state as State);

// A deeper route was successfully popped - rebuild the current level with the updated nested state.
if (nestedResult) {
const newRoutes = [...state.routes] as typeof state.routes;
// @ts-expect-error -- we're rebuilding a structurally identical route with updated nested state
newRoutes[index] = {...focusedRoute, state: nestedResult};
return {...state, routes: newRoutes, index} as State;
// Rebuild from the narrowed state to preserve full and partial route categories.
if (state.stale === false) {
const nestedResult = popFocusedRoute(focusedRoute.state);
const fullFocusedRoute = state.routes.at(index);
if (nestedResult && fullFocusedRoute) {
const newRoutes = [...state.routes];
newRoutes[index] = {...fullFocusedRoute, state: nestedResult};
return {...state, routes: newRoutes, index};
}
} else {
const partialFocusedRoute = state.routes.at(index);
const nestedResult = partialFocusedRoute?.state ? popFocusedRoute(partialFocusedRoute.state) : undefined;
if (nestedResult && partialFocusedRoute) {
const newRoutes = [...state.routes];
newRoutes[index] = {...partialFocusedRoute, state: nestedResult};
return {...state, routes: newRoutes, index};
}
}
}

// remove the focused route itself if siblings remain.
if (state.routes.length > 1) {
if (state.stale === false) {
const newRoutes = state.routes.filter((_, i) => i !== index);
return {...state, routes: newRoutes, index: newRoutes.length - 1};
}
const newRoutes = state.routes.filter((_, i) => i !== index);
return {...state, routes: newRoutes, index: newRoutes.length - 1} as State;
return {...state, routes: newRoutes, index: newRoutes.length - 1};
}

// Only one route at this level and nothing deeper to pop — signal the parent to remove this level entirely.
Expand All @@ -125,21 +138,21 @@ function popFocusedRoute(state: State): State | undefined {
function getPathFromStateWithDynamicRoute(state: State): string {
const focusedRoute = findFocusedRouteWithOnyxTabGuard(state);
const screenName = focusedRoute?.name ?? '';
const suffixPattern = normalizedConfigs[screenName as Screen]?.path;
const suffixPattern = hasKey(normalizedConfigs, screenName) ? normalizedConfigs[screenName]?.path : undefined;

if (!suffixPattern) {
return RNGetPathFromState(state, config);
}

let actualSuffix = buildSuffixFromPattern(suffixPattern, focusedRoute?.params as Record<string, unknown> | undefined);
let actualSuffix = buildSuffixFromPattern(suffixPattern, focusedRoute?.params ? {...focusedRoute.params} : undefined);

// If this dynamic screen hosts a tab navigator, append the focused tab's path segment.
if (screensWithOnyxTabNavigator.has(screenName)) {
const tabState = focusedRoute?.state;
if (tabState) {
const tabIndex = tabState.index ?? tabState.routes.length - 1;
const focusedTab = tabState.routes[tabIndex];
const tabPath = focusedTab && isScreen(focusedTab.name) ? normalizedConfigs[focusedTab.name]?.path : undefined;
const tabPath = focusedTab && hasKey(normalizedConfigs, focusedTab.name) ? normalizedConfigs[focusedTab.name]?.path : undefined;
if (tabPath) {
const [suffixPathOnly, suffixQueryOnly] = splitPathAndQuery(actualSuffix);
actualSuffix = `${suffixPathOnly}/${tabPath}${suffixQueryOnly ? `?${suffixQueryOnly}` : ''}`;
Expand Down Expand Up @@ -184,7 +197,7 @@ function getPathFromState(state: State): string {
const focusedRoute = findFocusedRouteWithOnyxTabGuard(state);
const screenName = focusedRoute?.name ?? '';

return isDynamicRouteScreen(screenName as Screen) ? getPathFromStateWithDynamicRoute(state) : RNGetPathFromState(state, config);
return hasKey(normalizedConfigs, screenName) && isDynamicRouteScreen(screenName) ? getPathFromStateWithDynamicRoute(state) : RNGetPathFromState(state, config);
}

export default getPathFromState;
3 changes: 1 addition & 2 deletions src/libs/Navigation/helpers/getStateFromPath.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Log from '@libs/Log';
import {linkingConfig} from '@libs/Navigation/linkingConfig';

import type {Route} from '@src/ROUTES';
import {DYNAMIC_ROUTES} from '@src/ROUTES';
import type {Screen} from '@src/SCREENS';
import SCREENS from '@src/SCREENS';
Expand All @@ -20,7 +19,7 @@ import getMatchingNewRoute from './getMatchingNewRoute';
* @param path - The path to parse
* @returns - It's possible that there is no navigation action for the given path
*/
function getStateFromPath(path: Route): PartialState<NavigationState> {
function getStateFromPath(path: string): PartialState<NavigationState> {
const normalizedPath = !path.startsWith('/') ? `/${path}` : path;
const normalizedPathAfterRedirection = getMatchingNewRoute(normalizedPath) ?? normalizedPath;

Expand Down
10 changes: 4 additions & 6 deletions src/libs/Navigation/helpers/linkTo/getMinimalAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import type {State} from '@navigation/types';
import type {NavigationAction, NavigationState} from '@react-navigation/native';
import type {Writable} from 'type-fest';

import type {ActionPayload} from './types';

type MinimalAction = {
action: Writable<NavigationAction>;
targetState: State | undefined;
Expand All @@ -30,15 +28,15 @@ function getMinimalAction(action: NavigationAction, state: NavigationState): Min
currentState = currentState?.routes[currentState.index ?? -1].state;
currentTargetKey = currentState?.key;

const payload = currentAction.payload as ActionPayload;
const params = 'params' in currentAction.payload && typeof currentAction.payload.params === 'object' ? currentAction.payload.params : undefined;

// Creating new smaller action
currentAction = {
type: currentAction.type,
payload: {
name: payload?.params?.screen,
params: payload?.params?.params,
path: payload?.params?.path,
name: params && 'screen' in params ? params.screen : undefined,
params: params && 'params' in params ? params.params : undefined,
path: params && 'path' in params ? params.path : undefined,
},
target: currentTargetKey,
};
Expand Down
Loading
Loading